#include "Fixture.h"
#include "ScopedFlags.h"
#include "doctest.h"
using namespace Luau;
LUAU_FASTFLAG(DebugLuauForceOldSolver)
LUAU_FASTFLAG(LuauExplicitTypeInstantiationSupport)
LUAU_FASTFLAG(LuauReplacerRespectsReboundGenerics)
LUAU_FASTFLAG(LuauVisitCallTypeArgsInDfg)
TEST_SUITE_BEGIN("TypeInferExplicitTypeInstantiations");
#define SUBCASE_BOTH_SOLVERS() \
for (bool enabled : {true, false}) \
if (ScopedFastFlag sffSolver{FFlag::DebugLuauForceOldSolver, !enabled}; true) \
SUBCASE(enabled ? "New solver" : "Old solver")
TEST_CASE_FIXTURE(Fixture, "as_expression_correct")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local function f<T>(): T
return nil :: any
end
local correct = f<<number>>() + 5
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
}
TEST_CASE_FIXTURE(Fixture, "as_expression_incorrect")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local function f<T>(): T
return nil :: any
end
local incorrect = f<<string>>() + 5
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (!FFlag::DebugLuauForceOldSolver)
{
REQUIRE_EQ(
toString(result.errors[0]),
"Operator '+' could not be applied to operands of types string and number; there is no corresponding overload for __add"
);
}
else
{
REQUIRE_EQ(toString(result.errors[0]), "Expected this to be 'number', but got 'string'");
}
}
}
TEST_CASE_FIXTURE(Fixture, "as_stmt_correct")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local function f<T>(a: T, b: T)
return nil :: any
end
f<<number | string>>(1, "a")
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
}
TEST_CASE_FIXTURE(Fixture, "as_stmt_incorrect")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local function f<T>(a: T, b: T)
return nil :: any
end
f<<number | boolean>>(1, "a")
)");
if (!FFlag::DebugLuauForceOldSolver)
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
std::string expected =
"Expected this to be 'boolean | number', but got 'string';\n"
"this is because\n"
"\t * the 1st component of the union is `number`, and `string` is not a subtype of `number`\n"
"\t * the 2nd component of the union is `boolean`, and `string` is not a subtype of `boolean`"
;
CHECK_LONG_STRINGS_EQ(expected, toString(result.errors.at(0)));
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE_EQ(
toString(result.errors[0]), "Expected this to be 'boolean | number', but got 'string'; none of the union options are compatible"
);
}
}
}
TEST_CASE_FIXTURE(Fixture, "multiple_calls")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local function f<T>(): T
return nil :: any
end
local a: number = f<<number>>()
local b: string = f<<string>>()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
}
TEST_CASE_FIXTURE(Fixture, "anonymous_type_inferred")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local function f<T, U>(): { a: T, b: U }
return nil :: any
end
local correct: { a: number, b: string } = f<<number>>()
local incorrect: { a: number, b: string } = f<<string>>()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE_EQ(result.errors[0].location.begin.line, 7);
LUAU_REQUIRE_ERROR(result, TypeMismatch);
}
}
TEST_CASE_FIXTURE(Fixture, "type_packs")
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
ScopedFastFlag oldSolver{FFlag::DebugLuauForceOldSolver, true};
CheckResult result = check(R"(
--!strict
local function f<T..., U...>(...: T...): U... end
local a: number, b: string = f<<(boolean, {}), (number, string)>>(true, {})
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "type_packs_method")
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
ScopedFastFlag oldSolver{FFlag::DebugLuauForceOldSolver, true};
CheckResult result = check(R"(
--!strict
local t: {
f: <T..., U...>(self: any, T...) -> U...,
} = nil :: any
local a: number, b: string = t:f<<(boolean, {}), (number, string)>>(true, {})
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "type_packs_incorrect")
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
ScopedFastFlag oldSolver{FFlag::DebugLuauForceOldSolver, true};
CheckResult result = check(R"(
--!strict
local function f<T..., U...>(...: T...): U... end
local a: number, b: string = f<<(boolean, {}), (number, string)>>(true, "uh oh")
)");
LUAU_REQUIRE_ERROR(result, TypeMismatch);
}
TEST_CASE_FIXTURE(Fixture, "type_packs_incorrect_method")
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
ScopedFastFlag oldSolver{FFlag::DebugLuauForceOldSolver, true};
CheckResult result = check(R"(
--!strict
local t: {
f: <T..., U...>(self: any, T...) -> U...,
} = nil :: any
local a: number, b: string = t:f<<(boolean, {}), (number, string)>>(true, "uh oh")
)");
LUAU_REQUIRE_ERROR(result, TypeMismatch);
}
TEST_CASE_FIXTURE(Fixture, "dot_index_call")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local t = {
f = function<T>(): T
return nil :: any
end,
}
local correct: number = t.f<<number>>()
local incorrect: number = t.f<<string>>()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
REQUIRE_EQ(result.errors[0].location.begin.line, 9);
}
}
TEST_CASE_FIXTURE(Fixture, "method_index_call")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local t = {
f = function<T>(self: any): T
return nil :: any
end,
}
local correct: number = t:f<<number>>()
local incorrect: number = t:f<<string>>()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR(result, TypeMismatch);
REQUIRE_EQ(result.errors[0].location.begin.line, 9);
}
}
TEST_CASE_FIXTURE(Fixture, "stored_as_variable")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local function f<T>(): T
return nil :: any
end
local fNumber = f<<number>>
local correct: number = fNumber()
local incorrect: string = fNumber()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR(result, TypeMismatch);
REQUIRE_EQ(result.errors[0].location.begin.line, 9);
}
}
TEST_CASE_FIXTURE(Fixture, "not_a_function")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local oops = 3
local stub = oops<<number>>
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR(result, InstantiateGenericsOnNonFunction);
REQUIRE_EQ(toString(result.errors[0]), "Cannot instantiate type parameters on something without type parameters.");
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "metatable_call")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local t = setmetatable({}, {
__call = function<T>(self): T
return nil :: any
end,
})
t<<number>>()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR(result, InstantiateGenericsOnNonFunction);
REQUIRE_EQ(toString(result.errors[0]), "Luau does not currently support explicitly instantiating a table with a `__call` metamethod. \
You may be able to work around this by creating a function that calls the table, and using that instead.");
}
}
TEST_CASE_FIXTURE(Fixture, "method_call_incomplete")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local t = {
f = function<T, U>(self: any): T | U
return nil :: any
end,
}
local correct: number | string = t:f<<number>>()
local incorrect: number | string = t:f<<boolean>>()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR(result, TypeMismatch);
REQUIRE_EQ(result.errors[0].location.begin.line, 9);
}
}
TEST_CASE_FIXTURE(Fixture, "too_many_provided")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local function f<T>() end
f<<number, string>>()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR(result, TypeInstantiationCountMismatch);
if (!FFlag::DebugLuauForceOldSolver)
{
REQUIRE_EQ(
toString(result.errors[0]),
"Too many type parameters passed to 'f', which is typed as <T>(...any) -> (). Expected at most 1 type parameter, but 2 provided."
);
}
else
{
REQUIRE_EQ(
toString(result.errors[0]),
"Too many type parameters passed to 'f', which is typed as <T>() -> (). Expected at most 1 type parameter, but 2 provided."
);
}
}
}
TEST_CASE_FIXTURE(Fixture, "too_many_provided_type_packs")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local function f<T...>(): (T...) end
f<<(string, number), (true, false)>>()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR(result, TypeInstantiationCountMismatch);
if (!FFlag::DebugLuauForceOldSolver)
{
REQUIRE_EQ(
toString(result.errors[0]),
"Too many type parameters passed to 'f', which is typed as <T...>(...any) -> (T...). Expected at most 1 type pack, but 2 provided."
);
}
else
{
REQUIRE_EQ(
toString(result.errors[0]),
"Too many type parameters passed to 'f', which is typed as <T...>() -> (T...). Expected at most 1 type pack, but 2 provided."
);
}
}
}
TEST_CASE_FIXTURE(Fixture, "too_many_provided_method")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local t = {
f = function<T>(self: any) end,
}
t:f<<number, string>>()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR(result, TypeInstantiationCountMismatch);
REQUIRE_EQ(result.errors[0].location.begin.line, 6);
if (!FFlag::DebugLuauForceOldSolver)
{
REQUIRE_EQ(
toString(result.errors[0]),
"Too many type parameters passed to function typed as <T>(any) -> (). Expected at most 1 type parameter, but 2 provided."
);
}
else
{
REQUIRE_EQ(
toString(result.errors[0]),
"Too many type parameters passed to 't.f', which is typed as <T>(any) -> (). Expected at most 1 type parameter, but 2 provided."
);
}
}
}
TEST_CASE_FIXTURE(Fixture, "too_many_type_packs_provided_method")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local t = {
f = function<T...>(self: any): (T...) end,
}
t:f<<(number, string), (true, false)>>()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR(result, TypeInstantiationCountMismatch);
REQUIRE_EQ(result.errors[0].location.begin.line, 6);
if (!FFlag::DebugLuauForceOldSolver)
{
REQUIRE_EQ(
toString(result.errors[0]),
"Too many type parameters passed to function typed as <T...>(any) -> (T...). Expected at most 1 type pack, but 2 provided."
);
}
else
{
REQUIRE_EQ(
toString(result.errors[0]),
"Too many type parameters passed to 't.f', which is typed as <T...>(any) -> (T...). Expected at most 1 type pack, but 2 provided."
);
}
}
}
TEST_CASE_FIXTURE(Fixture, "function_intersections")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
--!strict
local f: (<T>(T) -> T) & (<T>(T?) -> T) = nil :: any
f<<number>>()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR(result, InstantiateGenericsOnNonFunction);
REQUIRE_EQ(result.errors[0].location.begin.line, 3);
REQUIRE_EQ(toString(result.errors[0]), "Luau does not currently support explicitly instantiating an overloaded function type.");
}
}
TEST_CASE_FIXTURE(Fixture, "incomplete_type_packs")
{
SUBCASE_BOTH_SOLVERS()
{
ScopedFastFlag semantics{FFlag::LuauExplicitTypeInstantiationSupport, true};
CheckResult result = check(R"(
local f: <A, T...>() -> (A, T...) = nil :: any
local correct: string, b: number, c: boolean = f<<string>>()
local incorrect: number, b: number, c: boolean = f<<string>>()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
LUAU_REQUIRE_ERROR(result, TypeMismatch);
REQUIRE_EQ(result.errors[0].location.begin.line, 3);
}
}
TEST_CASE_FIXTURE(Fixture, "replacing_generic_with_generic")
{
ScopedFastFlag sffs[] = {
{FFlag::DebugLuauForceOldSolver, false},
{FFlag::LuauExplicitTypeInstantiationSupport, true},
{FFlag::LuauReplacerRespectsReboundGenerics, true},
};
CheckResult result = check(R"(
local foo: <A, B>() -> (A, B) = nil :: any
local function bar<T>()
return foo<<T, number>>()
end
local baz, quxx = bar<<string>>()
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string", toString(requireType("baz")));
CHECK_EQ("number", toString(requireType("quxx")));
}
TEST_CASE_FIXTURE(Fixture, "typeof_in_method_call_type_args_no_crash")
{
ScopedFastFlag sffs[] = {
{FFlag::LuauExplicitTypeInstantiationSupport, true},
{FFlag::LuauVisitCallTypeArgsInDfg, true},
};
CheckResult result = check(R"(
local t = {}
function t:f<T, U>() end
local x = 5
globl = 42
t:f<<typeof(x), string>>()
t:f<<number, typeof(x)>>()
t:f<<typeof(globl), unknown>>()
t:f<<typeof(t.f), unknown>>()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(get<UnknownSymbol>(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "typeof_local_in_type_pack_no_crash")
{
ScopedFastFlag sffs[] = {
{FFlag::LuauExplicitTypeInstantiationSupport, true},
{FFlag::LuauVisitCallTypeArgsInDfg, true},
};
CheckResult result = check(R"(
local t = {}
function t:f<T...>() end
local x = 5
t:f<<(string, typeof(x))>>()
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();