mirror of
https://github.com/luau-lang/luau.git
synced 2024-11-15 14:25:44 +08:00
c799a548e4
> What's new? * Fragment Autocomplete: a new API allows for type checking a small fragment of code against an existing file, significantly speeding up autocomplete performance in large files. > New Solver * E-Graphs have landed: this is an ongoing approach to make the new type solver simplify types in a more consistent and principled manner, based on similar work (e.g.: https://egraphs-good.github.io/). * Adds support for exported / local user type functions. * Fixes a set of bugs in which the new solver will fail to complete inference for simple expressions with just literals and operators. > General * It is now an explicit runtime error to `require` a path with a ".lua" or ".luau" extension, and the error message will suggest removing the extension. ``` require("path/to/mymodule.lua") ``` * Fixes a bug in which whether two `Symbol`s are equal depends on whether the new solver is enabled.
3040 lines
83 KiB
C++
3040 lines
83 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
|
|
#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 "Luau/VisitType.h"
|
|
|
|
#include "ClassFixture.h"
|
|
#include "Fixture.h"
|
|
|
|
#include "ScopedFlags.h"
|
|
#include "doctest.h"
|
|
|
|
using namespace Luau;
|
|
|
|
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
|
|
LUAU_FASTFLAG(LuauSolverV2)
|
|
LUAU_FASTINT(LuauTarjanChildLimit)
|
|
LUAU_FASTFLAG(LuauRetrySubtypingWithoutHiddenPack)
|
|
LUAU_FASTFLAG(LuauDontRefCountTypesInTypeFunctions)
|
|
|
|
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::LuauSolverV2)
|
|
{
|
|
const TypePackMismatch* tm = get<TypePackMismatch>(result.errors[0]);
|
|
REQUIRE(tm);
|
|
CHECK(toString(tm->wantedTp) == "number");
|
|
CHECK(toString(tm->givenTp) == "boolean");
|
|
}
|
|
else
|
|
{
|
|
CHECK_EQ(
|
|
result.errors[0],
|
|
(TypeError{
|
|
Location{Position{3, 16}, Position{3, 20}},
|
|
TypeMismatch{
|
|
builtinTypes->numberType,
|
|
builtinTypes->booleanType,
|
|
}
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "cannot_hoist_interior_defns_into_signature")
|
|
{
|
|
// This test verifies that the signature does not have access to types
|
|
// declared within the body. Under DCR, if the function's inner scope
|
|
// encompasses the entire function expression, it would be possible for this
|
|
// to type check (but the solver output is somewhat undefined). This test
|
|
// ensures that this isn't the case.
|
|
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());
|
|
|
|
REQUIRE_EQ(*follow(retVec[0]), *builtinTypes->numberType);
|
|
}
|
|
|
|
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_EQ(*builtinTypes->numberType, *follow(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{builtinTypes->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);
|
|
|
|
TypeId fooTy = tt->props.at("foo").type();
|
|
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"));
|
|
TypeId k = ttv->props["f"].type();
|
|
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::LuauSolverV2)
|
|
{
|
|
GenericError* g = get<GenericError>(result.errors[0]);
|
|
REQUIRE(g);
|
|
CHECK(g->message == "None of the overloads for function that accept 1 arguments are compatible.");
|
|
}
|
|
else
|
|
{
|
|
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
|
|
REQUIRE(tm);
|
|
CHECK_EQ(builtinTypes->numberType, tm->wantedType);
|
|
CHECK_EQ(builtinTypes->stringType, tm->givenType);
|
|
}
|
|
|
|
ExtraInformation* ei = get<ExtraInformation>(result.errors[1]);
|
|
REQUIRE(ei);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK("Available overloads: (number) -> number; (number) -> string; and (number, number) -> number" == 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(builtinTypes->numberType, tm->wantedType);
|
|
CHECK_EQ(builtinTypes->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")
|
|
{
|
|
// This is not part of the new non-strict specification currently.
|
|
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];
|
|
|
|
// Ensure the location matches the location of the function identifier
|
|
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];
|
|
|
|
// Ensure the location matches the location of the expression returning the function
|
|
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);
|
|
}
|
|
|
|
// FIXME: This and the above case get handled very differently. It's pretty dumb.
|
|
// We really should unify the two code paths, probably by deleting AstStatFunction.
|
|
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);
|
|
}
|
|
|
|
// We had a bug where we'd look up the type of a recursive call using the DFG,
|
|
// not the bindings tables. As a result, we would erroneously use the
|
|
// generalized type of foo() in this recursive fragment. This creates a
|
|
// constraint cycle that doesn't always work itself out.
|
|
//
|
|
// The fix is for the DFG node within the scope of foo() to retain the
|
|
// ungeneralized type of foo.
|
|
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::LuauSolverV2)
|
|
{
|
|
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")
|
|
{
|
|
// This is not part of the spec for the new non-strict mode currently.
|
|
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")
|
|
{
|
|
// CLI-114134: this code *probably* wants the egraph in order
|
|
// to work properly. The new solver either falls over or
|
|
// forces so many constraints as to be unreliable.
|
|
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")
|
|
{
|
|
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
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
const FunctionType* ftv = get<FunctionType>(requireType("swapTwice"));
|
|
REQUIRE(ftv != nullptr);
|
|
|
|
std::vector<TypeId> argVec = flatten(ftv->argTypes).first;
|
|
|
|
REQUIRE_EQ(1, argVec.size());
|
|
|
|
const TableType* argType = get<TableType>(follow(argVec[0]));
|
|
REQUIRE_MESSAGE(argType != nullptr, argVec[0]);
|
|
|
|
CHECK(bool(argType->indexer));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "higher_order_function_4")
|
|
{
|
|
// CLI-114134: this code *probably* wants the egraph in order
|
|
// to work properly. The new solver either falls over or
|
|
// forces so many constraints as to be unreliable.
|
|
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);
|
|
|
|
/*
|
|
* mergesort takes two arguments: an array of some type T and a function that takes two Ts.
|
|
* We must assert that these two types are in fact the same type.
|
|
* In other words, comp(arr[x], arr[y]) is well-typed.
|
|
*/
|
|
|
|
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_EQ(*arg0->indexer->indexResultType, *arg1Args[0]);
|
|
CHECK_EQ(*arg0->indexer->indexResultType, *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")
|
|
{
|
|
// new non-strict mode spec does not include this error yet.
|
|
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{
|
|
builtinTypes->numberType,
|
|
builtinTypes->stringType,
|
|
}
|
|
})
|
|
);
|
|
|
|
CHECK_EQ(
|
|
result.errors[1],
|
|
(TypeError{
|
|
Location{Position{3, 20}, Position{3, 23}},
|
|
TypeMismatch{
|
|
builtinTypes->stringType,
|
|
builtinTypes->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::LuauSolverV2)
|
|
CHECK_EQ("{string}", toString(requireType("tab"), opts));
|
|
else
|
|
CHECK_EQ("{any}", toString(requireType("tab"), opts));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "too_many_return_values")
|
|
{
|
|
// FIXME: CLI-116157 variadic and generic type packs seem to be interacting incorrectly.
|
|
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_in_parentheses")
|
|
{
|
|
// FIXME: CLI-116157 variadic and generic type packs seem to be interacting incorrectly.
|
|
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")
|
|
{
|
|
// FIXME: CLI-116157 variadic and generic type packs seem to be interacting incorrectly.
|
|
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::LuauSolverV2)
|
|
{
|
|
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::LuauSolverV2)
|
|
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::LuauSolverV2)
|
|
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::LuauSolverV2)
|
|
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);
|
|
|
|
// AstExprCall is the node that has the overload stored on it.
|
|
// findTypeAtPosition will look at the AstExprLocal, but this is not what
|
|
// we want to look at.
|
|
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")));
|
|
// the new solver does not currently "favor" arity-matching overloads when the call itself is ill-typed.
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ("string", toString(requireType("y")));
|
|
else
|
|
CHECK_EQ("number", toString(requireType("y")));
|
|
// Should this be string|number?
|
|
CHECK_EQ("string", toString(requireType("z")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "infer_anonymous_function_arguments")
|
|
{
|
|
// FIXME: CLI-116133 bidirectional type inference needs to push expected types in for higher-order function calls
|
|
DOES_NOT_PASS_NEW_SOLVER_GUARD();
|
|
|
|
// Simple direct arg to arg propagation
|
|
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);
|
|
|
|
// An optional function is accepted, but since we already provide a function, nil can be ignored
|
|
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);
|
|
|
|
// Make sure self calls match correct index
|
|
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);
|
|
|
|
// Mix inferred and explicit argument types
|
|
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);
|
|
|
|
// Anonymous function has a variadic pack
|
|
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]));
|
|
|
|
// Can't accept more arguments than provided
|
|
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 = R"(Type
|
|
'<a>(number, number, a) -> number'
|
|
could not be converted into
|
|
'(number, number) -> number'
|
|
caused by:
|
|
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)";
|
|
}
|
|
else
|
|
{
|
|
expected = R"(Type
|
|
'(number, number, *error-type*) -> number'
|
|
could not be converted into
|
|
'(number, number) -> number'
|
|
caused by:
|
|
Argument count mismatch. Function expects 3 arguments, but only 2 are specified)";
|
|
}
|
|
|
|
CHECK_EQ(expected, toString(result.errors[0]));
|
|
|
|
// Infer from variadic packs into elements
|
|
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);
|
|
|
|
// Infer from variadic packs into variadic packs
|
|
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]));
|
|
|
|
// Return type inference
|
|
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("Type 'number' could not be converted into 'Table'", toString(result.errors[0]));
|
|
|
|
// Return type doesn't inference 'nil'
|
|
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")
|
|
{
|
|
// FIXME: CLI-116133 bidirectional type inference needs to push expected types in for higher-order function calls
|
|
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")
|
|
{
|
|
// FIXME: CLI-116133 bidirectional type inference needs to push expected types in for higher-order function calls
|
|
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")
|
|
{
|
|
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_NO_ERRORS(result);
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
// https://github.com/luau-lang/luau/issues/767
|
|
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")
|
|
{
|
|
// FIXME: CLI-116133 bidirectional type inference needs to push expected types in for higher-order function calls
|
|
DOES_NOT_PASS_NEW_SOLVER_GUARD();
|
|
|
|
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
|
|
)");
|
|
|
|
// `h` regresses in the new solver, the return type is not being pushed into the body.
|
|
if (FFlag::LuauSolverV2)
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
else
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_arg_count")
|
|
{
|
|
// FIXME: CLI-116111 test disabled until type path stringification is improved
|
|
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 = R"(Type
|
|
'(number, number) -> string'
|
|
could not be converted into
|
|
'(number) -> string'
|
|
caused by:
|
|
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")
|
|
{
|
|
// FIXME: CLI-116111 test disabled until type path stringification is improved
|
|
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 = R"(Type
|
|
'(number, number) -> string'
|
|
could not be converted into
|
|
'(number, string) -> string'
|
|
caused by:
|
|
Argument #2 type is not compatible.
|
|
Type 'string' could not be converted into 'number')";
|
|
CHECK_EQ(expected, toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_count")
|
|
{
|
|
// FIXME: CLI-116111 test disabled until type path stringification is improved
|
|
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 = R"(Type
|
|
'(number, number) -> number'
|
|
could not be converted into
|
|
'(number, number) -> (number, boolean)'
|
|
caused by:
|
|
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")
|
|
{
|
|
// FIXME: CLI-116111 test disabled until type path stringification is improved
|
|
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 = R"(Type
|
|
'(number, number) -> string'
|
|
could not be converted into
|
|
'(number, number) -> number'
|
|
caused by:
|
|
Return type is not compatible.
|
|
Type 'string' could not be converted into 'number')";
|
|
CHECK_EQ(expected, toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "error_detailed_function_mismatch_ret_mult")
|
|
{
|
|
// FIXME: CLI-116111 test disabled until type path stringification is improved
|
|
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 = R"(Type
|
|
'(number, number) -> (number, string)'
|
|
could not be converted into
|
|
'(number, number) -> (number, boolean)'
|
|
caused by:
|
|
Return #2 type is not compatible.
|
|
Type 'string' could not be converted into 'boolean')";
|
|
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);
|
|
|
|
// if 'string' library property was replaced with an internal module type, it will be freed and the next check will crash
|
|
frontend.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::LuauSolverV2)
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK_EQ(
|
|
toString(result.errors[0]),
|
|
R"(Type function instance add<a, number> depends on generic function parameters but does not appear in the function signature; this construct cannot be type-checked at this time)"
|
|
);
|
|
CHECK_EQ(
|
|
toString(result.errors[1]),
|
|
R"(Type function instance add<a, number> depends on generic function parameters but does not appear in the function signature; this construct cannot be type-checked at this time)"
|
|
);
|
|
}
|
|
else
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK_EQ(toString(result.errors[0]), R"(Type 'string' could not be converted into 'number')");
|
|
CHECK_EQ(toString(result.errors[1]), R"(Type 'string' could not be converted into 'number')");
|
|
}
|
|
}
|
|
|
|
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
|
|
)");
|
|
|
|
// LUAU_REQUIRE_NO_ERRORS is particularly unhelpful when this test is broken.
|
|
// You get a TypeMismatch error where both types stringify the same.
|
|
|
|
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")
|
|
{
|
|
// This test regresses in the new solver, but is sort of nonsensical insofar as `foo` is known to be `nil`, so it's "right" to not be able to call
|
|
// it.
|
|
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")
|
|
{
|
|
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::LuauSolverV2)
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ(
|
|
toString(result.errors[0]),
|
|
R"(Type function instance add<a, number> depends on generic function parameters but does not appear in the function signature; this construct cannot be type-checked at this time)"
|
|
);
|
|
}
|
|
else
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK_EQ(toString(result.errors[0]), R"(Type
|
|
'(string) -> string'
|
|
could not be converted into
|
|
'((number) -> number)?'
|
|
caused by:
|
|
None of the union options are compatible. For example:
|
|
Type
|
|
'(string) -> string'
|
|
could not be converted into
|
|
'(number) -> number'
|
|
caused by:
|
|
Argument #1 type is not compatible.
|
|
Type 'number' could not be converted into 'string')");
|
|
CHECK_EQ(toString(result.errors[1]), R"(Type 'string' could not be converted into 'number')");
|
|
}
|
|
}
|
|
|
|
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")
|
|
{
|
|
// FIXME: CLI-116122 bug where `t:b` does not check against the type from the indexer annotation on `t`.
|
|
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(
|
|
R"(Type
|
|
'(*error-type*) -> number'
|
|
could not be converted into
|
|
'() -> number'
|
|
caused by:
|
|
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")
|
|
{
|
|
// FIXME: CLI-116157 variadic and generic type packs seem to be interacting incorrectly.
|
|
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")
|
|
{
|
|
// FIXME: CLI-116157 variadic and generic type packs seem to be interacting incorrectly.
|
|
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")
|
|
{
|
|
// This test only makes sense for the old solver
|
|
if (FFlag::LuauSolverV2)
|
|
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
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("<a>(a) -> a", toString(requireType("f")));
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ("({ read p: { read q: unknown } }) -> ~(false?)?", toString(requireType("g")));
|
|
else
|
|
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::LuauSolverV2)
|
|
{
|
|
// These improvements to the error messages are currently regressed in the new type solver.
|
|
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");
|
|
}
|
|
}
|
|
|
|
// This might be surprising, but since 'any' became optional, unannotated functions in non-strict 'expect' 0 arguments
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "improved_function_arg_mismatch_error_nonstrict")
|
|
{
|
|
// This behavior is not part of the current specification of the new type solver.
|
|
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")
|
|
{
|
|
// The case that _should_ succeed here (`z = x`) does not currently in the new solver.
|
|
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 = R"(Type
|
|
'(("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)'
|
|
could not be converted into
|
|
'("blue" | "red") -> ("blue" | "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(&frontend);
|
|
|
|
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(&frontend);
|
|
|
|
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(&frontend);
|
|
|
|
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(BuiltinsFixture, "dont_assert_when_the_tarjan_limit_is_exceeded_during_generalization")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
ScopedFastInt sfi{FInt::LuauTarjanChildLimit, 1};
|
|
|
|
CheckResult result = check(R"(
|
|
function f(t)
|
|
t.x.y.z = 441
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
CHECK_MESSAGE(get<UnificationTooComplex>(result.errors[0]), "Expected UnificationTooComplex but got: " << toString(result.errors[0]));
|
|
}
|
|
|
|
/* We had a bug under DCR where instantiated type packs had a nullptr scope.
|
|
*
|
|
* This caused an issue with promotion.
|
|
*/
|
|
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::LuauSolverV2)
|
|
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::LuauSolverV2)
|
|
CHECK(toString(requireType("foo")) == "((unknown) -> nil)?");
|
|
else
|
|
{
|
|
// note that b is not in the generic list; it is free, the unconstrained type of `bar`.
|
|
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")
|
|
{
|
|
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::LuauSolverV2)
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
auto tm = get<TypeMismatch>(result.errors[0]);
|
|
REQUIRE(tm);
|
|
CHECK("number" == toString(tm->wantedType));
|
|
CHECK("{ x: number }" == toString(tm->givenType));
|
|
}
|
|
else
|
|
{
|
|
// In the old solver, this produces a very strange result:
|
|
//
|
|
// Here, we instantiate `<a>(x: a, y: a?) -> a` with a fresh type `'a` for `a`.
|
|
// In argument #1, we unify `vec2` with `'a`.
|
|
// This is ok, so we record an equality constraint `'a` with `vec2`.
|
|
// In argument #2, we unify `{ x: number }` with `'a?`.
|
|
// This fails because `'a` has equality constraint with `vec2`,
|
|
// so `{ x: number } <: vec2?`, which is false.
|
|
//
|
|
// If the unifications were to be committed, then it'd result in the following type error:
|
|
//
|
|
// Type '{ x: number }' could not be converted into 'vec2?'
|
|
// caused by:
|
|
// [...] Table type '{ x: number }' not compatible with type 'vec2' because the former is missing field 'y'
|
|
//
|
|
// However, whenever we check the argument list, if there's an error, we don't commit the unifications, so it actually looks like this:
|
|
//
|
|
// Type '{ x: number }' could not be converted into 'a?'
|
|
// caused by:
|
|
// [...] Table type '{ x: number }' not compatible with type 'vec2' because the former is missing field 'y'
|
|
//
|
|
// Then finally, that generic is left floating free, and since the function returns that generic,
|
|
// that free type is then later bound to `number`, which succeeds and mutates the type graph.
|
|
// This again changes the type error where `a` becomes bound to `number`.
|
|
//
|
|
// Type '{ x: number }' could not be converted into 'number?'
|
|
// caused by:
|
|
// [...] Table type '{ x: number }' not compatible with type 'vec2' because the former is missing field 'y'
|
|
//
|
|
// Uh oh, that type error is extremely confusing for people who doesn't know how that went down.
|
|
// Really, what should happen is we roll each argument incompatibility into a union type, but that needs local type inference.
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
|
|
const std::string expected = R"(Type '{ x: number }' could not be converted into 'vec2?'
|
|
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("Type 'vec2' could not be converted into 'number'", 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::LuauSolverV2)
|
|
{
|
|
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]), "Type 'string' could not be converted into 'number'");
|
|
CHECK_EQ(toString(result.errors[1]), "Type 'number' could not be converted into 'boolean'");
|
|
}
|
|
}
|
|
|
|
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::LuauSolverV2)
|
|
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 sff{FFlag::LuauSolverV2, 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
|
|
|
|
apply(add, 5)
|
|
)");
|
|
|
|
// FIXME: this errored at some point, but doesn't anymore.
|
|
// the desired behavior here is erroring.
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
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);
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK(toString(result.errors.at(0)) == "Type pack 'string' could not be converted into 'number'; at [0], string is not a subtype of number");
|
|
else
|
|
CHECK_EQ("Type 'string' could not be converted into 'number'", 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);
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK(toString(result.errors.at(0)) == "Type pack 'string' could not be converted into 'number'; at [0], string is not a subtype of number");
|
|
else
|
|
CHECK_EQ("Type 'string' could not be converted into 'number'", 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")
|
|
{
|
|
// CLI-117118 - TypeInferFunctions.simple_unannotated_mutual_recursion relies on unstable assertions to pass.
|
|
if (FFlag::LuauSolverV2)
|
|
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::LuauSolverV2)
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(5, result);
|
|
// CLI-117117 Constraint solving is incomplete inTypeInferFunctions.simple_unannotated_mutual_recursion
|
|
CHECK(get<ConstraintSolvingIncompleteError>(result.errors[0]));
|
|
// This check is unstable between different machines and different runs of DCR because it depends on string equality between
|
|
// blocked type numbers, which is not guaranteed.
|
|
bool r = toString(result.errors[1]) == "Type pack '*blocked-tp-1*' could not be converted into 'boolean'; 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::LuauSolverV2, true},
|
|
{FFlag::LuauDontRefCountTypesInTypeFunctions, true}
|
|
};
|
|
|
|
// CLI-114134: This test:
|
|
// a) Has a kind of weird result (suggesting `number | false` is not great);
|
|
// b) Is force solving some constraints.
|
|
// We end up with a weird recursive type that, if you roughly look at it, is
|
|
// clearly `number`. Hopefully the egraph will be able to unfold this.
|
|
|
|
CheckResult result = check(R"(
|
|
function fib(n)
|
|
return n < 2 and 1 or fib(n-1) + fib(n-2)
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERRORS(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::LuauSolverV2)
|
|
return;
|
|
CheckResult result = check(R"(
|
|
function fib(n, u)
|
|
return (n or u) and (n < u and n + fib(n,u))
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
auto err = get<ExplicitFunctionAnnotationRecommended>(result.errors.back());
|
|
LUAU_ASSERT(err);
|
|
CHECK("number" == toString(err->recommendedReturn));
|
|
REQUIRE(err->recommendedArgs.size() == 2);
|
|
CHECK("number" == toString(err->recommendedArgs[0].second));
|
|
CHECK("number" == toString(err->recommendedArgs[1].second));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "tf_suggest_arg_type_2")
|
|
{
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
// Make sure the error types are cloned to module interface
|
|
frontend.options.retainFullTypeGraphs = false;
|
|
|
|
CheckResult result = check(R"(
|
|
local function escape_fslash(pre)
|
|
return (#pre % 2 == 0 and '\\' or '') .. pre .. '.'
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
auto err = get<NotATable>(result.errors.back());
|
|
REQUIRE(err);
|
|
CHECK("a" == toString(err->ty));
|
|
}
|
|
|
|
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()
|
|
)");
|
|
|
|
// This test verifies that an ICE doesn't occur, so the bulk of the test is
|
|
// just from running check above.
|
|
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::LuauSolverV2)
|
|
{
|
|
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(ClassFixture, "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::LuauSolverV2)
|
|
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
|
|
)");
|
|
|
|
// We only care that type checking completes without tripping a crash or an assertion.
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "function_definition_in_a_do_block")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local f
|
|
do
|
|
function f()
|
|
end
|
|
end
|
|
f()
|
|
)");
|
|
|
|
// We are predominantly interested in this test not crashing.
|
|
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()
|
|
)");
|
|
|
|
// We are predominantly interested in this test not crashing.
|
|
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()
|
|
)");
|
|
|
|
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")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDontRefCountTypesInTypeFunctions, true};
|
|
|
|
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
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
// The new solver should ideally be able to do better here, but this is no worse than the old solver.
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
|
|
auto tm1 = get<TypePackMismatch>(result.errors[0]);
|
|
REQUIRE(tm1);
|
|
CHECK(toString(tm1->wantedTp) == "string");
|
|
CHECK(toString(tm1->givenTp) == "boolean");
|
|
|
|
auto tm2 = get<TypePackMismatch>(result.errors[1]);
|
|
REQUIRE(tm2);
|
|
CHECK(toString(tm2->wantedTp) == "string");
|
|
CHECK(toString(tm2->givenTp) == "(buffer | class | function | number | string | table | thread | true) & unknown");
|
|
}
|
|
else
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
const TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
|
|
REQUIRE(tm);
|
|
CHECK(toString(tm->wantedType) == "string");
|
|
CHECK(toString(tm->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")
|
|
{
|
|
// This luau code should finish typechecking, not segfault upon dereferencing
|
|
// the normalized type
|
|
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(builtinTypes->numberType, requireType("v"));
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "hidden_variadics_should_not_break_subtyping")
|
|
{
|
|
// Only applies to new solver.
|
|
ScopedFastFlag sff{FFlag::LuauRetrySubtypingWithoutHiddenPack, true};
|
|
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
type FooType = {
|
|
SetValue: (Value: number) -> ()
|
|
}
|
|
|
|
local Foo: FooType = {
|
|
SetValue = function(Value: number)
|
|
|
|
end
|
|
}
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_SUITE_END();
|