luau/tests/TypeFunction.test.cpp
aaron 02241b6d24
Some checks failed
benchmark / callgrind (map[branch:main name:luau-lang/benchmark-data], ubuntu-22.04) (push) Has been cancelled
build / ${{matrix.os.name}} (map[name:macos version:macos-latest]) (push) Has been cancelled
build / ${{matrix.os.name}} (map[name:macos-arm version:macos-14]) (push) Has been cancelled
build / ${{matrix.os.name}} (map[name:ubuntu version:ubuntu-latest]) (push) Has been cancelled
build / windows (Win32) (push) Has been cancelled
build / windows (x64) (push) Has been cancelled
build / coverage (push) Has been cancelled
build / web (push) Has been cancelled
release / ${{matrix.os.name}} (map[name:macos version:macos-latest]) (push) Has been cancelled
release / ${{matrix.os.name}} (map[name:ubuntu version:ubuntu-20.04]) (push) Has been cancelled
release / ${{matrix.os.name}} (map[name:windows version:windows-latest]) (push) Has been cancelled
release / web (push) Has been cancelled
Sync to upstream/release/645 (#1440)
In this update, we continue to improve the overall stability of the new
type solver. We're also shipping some early bits of two new features,
one of the language and one of the analysis API: user-defined type
functions and an incremental typechecking API.

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

## New Solver

* Fix a crash where scopes are incorrectly accessed cross-module after
they've been deallocated by appropriately zeroing out associated scope
pointers for free types, generic types, table types, etc.
* Fix a crash where we were incorrectly caching results for bound types
in generalization.
* Eliminated some unnecessary intermediate allocations in the constraint
solver and type function infrastructure.
* Built some initial groundwork for an incremental typecheck API for use
by language servers.
* Built an initial technical preview for [user-defined type
functions](https://rfcs.luau-lang.org/user-defined-type-functions.html),
more work still to come (including calling type functions from other
type functions), but adventurous folks wanting to experiment with it can
try it out by enabling `FFlag::LuauUserDefinedTypeFunctionsSyntax` and
`FFlag::LuauUserDefinedTypeFunction` in their local environment. Special
thanks to @joonyoo181 who built up all the initial infrastructure for
this during his internship!

## Miscellaneous changes

* Fix a compilation error on Ubuntu (fixes #1437)

---

Internal Contributors:

Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: Jeremy Yoo <jyoo@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>

---------

Co-authored-by: Alexander McCord <amccord@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Vighnesh <vvijay@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: David Cope <dcope@roblox.com>
Co-authored-by: Lily Brown <lbrown@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
Co-authored-by: Junseo Yoo <jyoo@roblox.com>
2024-09-27 11:58:21 -07:00

1251 lines
37 KiB
C++

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypeFunction.h"
#include "Luau/ConstraintSolver.h"
#include "Luau/NotNull.h"
#include "Luau/Type.h"
#include "ClassFixture.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauUserDefinedTypeFunctions)
LUAU_DYNAMIC_FASTINT(LuauTypeFamilyApplicationCartesianProductLimit)
struct TypeFunctionFixture : Fixture
{
TypeFunction swapFunction;
TypeFunctionFixture()
: Fixture(true, false)
{
swapFunction = TypeFunction{
/* name */ "Swap",
/* reducer */
[](TypeId instance, const std::vector<TypeId>& tys, const std::vector<TypePackId>& tps, NotNull<TypeFunctionContext> ctx
) -> TypeFunctionReductionResult<TypeId>
{
LUAU_ASSERT(tys.size() == 1);
TypeId param = follow(tys.at(0));
if (isString(param))
{
return TypeFunctionReductionResult<TypeId>{ctx->builtins->numberType, false, {}, {}};
}
else if (isNumber(param))
{
return TypeFunctionReductionResult<TypeId>{ctx->builtins->stringType, false, {}, {}};
}
else if (is<BlockedType>(param) || is<PendingExpansionType>(param) || is<TypeFunctionInstanceType>(param) ||
(ctx->solver && ctx->solver->hasUnresolvedConstraints(param)))
{
return TypeFunctionReductionResult<TypeId>{std::nullopt, false, {param}, {}};
}
else
{
return TypeFunctionReductionResult<TypeId>{std::nullopt, true, {}, {}};
}
}
};
unfreeze(frontend.globals.globalTypes);
TypeId t = frontend.globals.globalTypes.addType(GenericType{"T"});
GenericTypeDefinition genericT{t};
ScopePtr globalScope = frontend.globals.globalScope;
globalScope->exportedTypeBindings["Swap"] =
TypeFun{{genericT}, frontend.globals.globalTypes.addType(TypeFunctionInstanceType{NotNull{&swapFunction}, {t}, {}})};
freeze(frontend.globals.globalTypes);
}
};
TEST_SUITE_BEGIN("TypeFunctionTests");
TEST_CASE_FIXTURE(TypeFunctionFixture, "basic_type_function")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type A = Swap<number>
type B = Swap<string>
type C = Swap<boolean>
local x = 123
local y: Swap<typeof(x)> = "foo"
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK("string" == toString(requireTypeAlias("A")));
CHECK("number" == toString(requireTypeAlias("B")));
CHECK("Swap<boolean>" == toString(requireTypeAlias("C")));
CHECK("string" == toString(requireType("y")));
CHECK("Type function instance Swap<boolean> is uninhabited" == toString(result.errors[0]));
};
TEST_CASE_FIXTURE(TypeFunctionFixture, "function_as_fn_ret")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local swapper: <T>(T) -> Swap<T>
local a = swapper(123)
local b = swapper("foo")
local c = swapper(false)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK("string" == toString(requireType("a")));
CHECK("number" == toString(requireType("b")));
CHECK("Swap<boolean>" == toString(requireType("c")));
CHECK("Type function instance Swap<boolean> is uninhabited" == toString(result.errors[0]));
}
TEST_CASE_FIXTURE(TypeFunctionFixture, "function_as_fn_arg")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local swapper: <T>(Swap<T>) -> T
local a = swapper(123)
local b = swapper(false)
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK("unknown" == toString(requireType("a")));
CHECK("unknown" == toString(requireType("b")));
CHECK("Type 'number' could not be converted into 'never'" == toString(result.errors[0]));
CHECK("Type 'boolean' could not be converted into 'never'" == toString(result.errors[1]));
}
TEST_CASE_FIXTURE(TypeFunctionFixture, "resolve_deep_functions")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local x: Swap<Swap<Swap<string>>>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("number" == toString(requireType("x")));
}
TEST_CASE_FIXTURE(TypeFunctionFixture, "unsolvable_function")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local impossible: <T>(Swap<T>) -> Swap<Swap<T>>
local a = impossible(123)
local b = impossible(true)
)");
LUAU_REQUIRE_ERROR_COUNT(6, result);
CHECK(toString(result.errors[0]) == "Type function instance Swap<Swap<T>> is uninhabited");
CHECK(toString(result.errors[1]) == "Type function instance Swap<T> is uninhabited");
CHECK(toString(result.errors[2]) == "Type function instance Swap<Swap<T>> is uninhabited");
CHECK(toString(result.errors[3]) == "Type function instance Swap<T> is uninhabited");
CHECK(toString(result.errors[4]) == "Type function instance Swap<Swap<T>> is uninhabited");
CHECK(toString(result.errors[5]) == "Type function instance Swap<T> is uninhabited");
}
TEST_CASE_FIXTURE(TypeFunctionFixture, "table_internal_functions")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local t: <T>({T}) -> {Swap<T>}
local a = t({1, 2, 3})
local b = t({"a", "b", "c"})
local c = t({true, false, true})
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(requireType("a")) == "{string}");
CHECK(toString(requireType("b")) == "{number}");
// FIXME: table types are constructing a trivial union here.
CHECK(toString(requireType("c")) == "{Swap<boolean | boolean | boolean>}");
CHECK(toString(result.errors[0]) == "Type function instance Swap<boolean | boolean | boolean> is uninhabited");
}
TEST_CASE_FIXTURE(TypeFunctionFixture, "function_internal_functions")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local f0: <T>(T) -> (() -> T)
local f: <T>(T) -> (() -> Swap<T>)
local a = f(1)
local b = f("a")
local c = f(true)
local d = f0(1)
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(requireType("a")) == "() -> string");
CHECK(toString(requireType("b")) == "() -> number");
CHECK(toString(requireType("c")) == "() -> Swap<boolean>");
CHECK(toString(result.errors[0]) == "Type function instance Swap<boolean> is uninhabited");
}
TEST_CASE_FIXTURE(Fixture, "add_function_at_work")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local function add(a, b)
return a + b
end
local a = add(1, 2)
local b = add(1, "foo")
local c = add("foo", 1)
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(toString(requireType("a")) == "number");
CHECK(toString(requireType("b")) == "add<number, string>");
CHECK(toString(requireType("c")) == "add<string, number>");
CHECK(
toString(result.errors[0]) ==
"Operator '+' could not be applied to operands of types number and string; there is no corresponding overload for __add"
);
CHECK(
toString(result.errors[1]) ==
"Operator '+' could not be applied to operands of types string and number; there is no corresponding overload for __add"
);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "cyclic_add_function_at_work")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type T = add<number | T, number>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireTypeAlias("T")) == "number");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "mul_function_with_union_of_multiplicatives")
{
if (!FFlag::LuauSolverV2)
return;
loadDefinition(R"(
declare class Vec2
function __mul(self, rhs: number): Vec2
end
declare class Vec3
function __mul(self, rhs: number): Vec3
end
)");
CheckResult result = check(R"(
type T = mul<Vec2 | Vec3, number>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireTypeAlias("T")) == "Vec2 | Vec3");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "mul_function_with_union_of_multiplicatives_2")
{
if (!FFlag::LuauSolverV2)
return;
loadDefinition(R"(
declare class Vec3
function __mul(self, rhs: number): Vec3
function __mul(self, rhs: Vec3): Vec3
end
)");
CheckResult result = check(R"(
type T = mul<number | Vec3, Vec3>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireTypeAlias("T")) == "Vec3");
}
TEST_CASE_FIXTURE(Fixture, "internal_functions_raise_errors")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local function innerSum(a, b)
local _ = a + b
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(
toString(result.errors[0]) ==
"Operator '+' could not be applied to operands of types unknown and unknown; there is no corresponding overload for __add"
);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "type_functions_can_be_shadowed")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type add<T> = string -- shadow add
-- this should be ok
function hi(f: add<unknown>)
return string.format("hi %s", f)
end
-- this should still work totally fine (and use the real type function)
function plus(a, b)
return a + b
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("hi")) == "(string) -> string");
CHECK(toString(requireType("plus")) == "<a, b>(a, b) -> add<a, b>");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "type_functions_inhabited_with_normalization")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local useGridConfig : any
local columns = useGridConfig("columns", {}) or 1
local gutter = useGridConfig('gutter', {}) or 0
local margin = useGridConfig('margin', {}) or 0
return function(frameAbsoluteWidth: number)
local cellAbsoluteWidth = (frameAbsoluteWidth - 2 * margin + gutter) / columns - gutter
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_function_works")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type KeysOfMyObject = keyof<MyObject>
local function ok(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
local function err(idx: KeysOfMyObject): "x" | "y" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"x\" | \"y\"", toString(tpm->wantedTp));
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_function_works_with_metatables")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local metatable = { __index = {w = 1} }
local obj = setmetatable({x = 1, y = 2, z = 3}, metatable)
type MyObject = typeof(obj)
type KeysOfMyObject = keyof<MyObject>
local function ok(idx: KeysOfMyObject): "w" | "x" | "y" | "z" return idx end
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->wantedTp));
CHECK_EQ("\"w\" | \"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_single_entry_no_uniontype")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local tbl_A = { abc = "value" }
local tbl_B = { a1 = nil, ["a2"] = nil }
type keyof_A = keyof<typeof(tbl_A)>
type keyof_B = keyof<typeof(tbl_B)>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireTypeAlias("keyof_A")) == "\"abc\"");
CHECK(toString(requireTypeAlias("keyof_B")) == "\"a1\" | \"a2\"");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_function_errors_if_it_has_nontable_part")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type KeysOfMyObject = keyof<MyObject | boolean>
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
)");
// FIXME(CLI-95289): we should actually only report the type function being uninhabited error at its first use, I think?
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(toString(result.errors[0]) == "Type 'MyObject | boolean' does not have keys, so 'keyof<MyObject | boolean>' is invalid");
CHECK(toString(result.errors[1]) == "Type 'MyObject | boolean' does not have keys, so 'keyof<MyObject | boolean>' is invalid");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_function_string_indexer")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type MyOtherObject = { [string]: number }
type KeysOfMyOtherObject = keyof<MyOtherObject>
type KeysOfMyObjects = keyof<MyObject | MyOtherObject>
local function ok(idx: KeysOfMyOtherObject): "z" return idx end
local function err(idx: KeysOfMyObjects): "z" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"z\"", toString(tpm->wantedTp));
CHECK_EQ("string", toString(tpm->givenTp));
tpm = get<TypePackMismatch>(result.errors[1]);
REQUIRE(tpm);
CHECK_EQ("\"z\"", toString(tpm->wantedTp));
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_function_common_subset_if_union_of_differing_tables")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type MyOtherObject = { w: number, y: number, z: number }
type KeysOfMyObject = keyof<MyObject | MyOtherObject>
local function err(idx: KeysOfMyObject): "z" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"z\"", toString(tpm->wantedTp));
CHECK_EQ("\"y\" | \"z\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_type_function_never_for_empty_table")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type KeyofEmpty = keyof<{}>
local foo = ((nil :: any) :: KeyofEmpty)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("foo")) == "never");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_function_works")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type KeysOfMyObject = rawkeyof<MyObject>
local function ok(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
local function err(idx: KeysOfMyObject): "x" | "y" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"x\" | \"y\"", toString(tpm->wantedTp));
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_function_ignores_metatables")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local metatable = { __index = {w = 1} }
local obj = setmetatable({x = 1, y = 2, z = 3}, metatable)
type MyObject = typeof(obj)
type KeysOfMyObject = rawkeyof<MyObject>
local function ok(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
local function err(idx: KeysOfMyObject): "x" | "y" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"x\" | \"y\"", toString(tpm->wantedTp));
CHECK_EQ("\"x\" | \"y\" | \"z\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_function_errors_if_it_has_nontable_part")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type KeysOfMyObject = rawkeyof<MyObject | boolean>
local function err(idx: KeysOfMyObject): "x" | "y" | "z" return idx end
)");
// FIXME(CLI-95289): we should actually only report the type function being uninhabited error at its first use, I think?
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(toString(result.errors[0]) == "Type 'MyObject | boolean' does not have keys, so 'rawkeyof<MyObject | boolean>' is invalid");
CHECK(toString(result.errors[1]) == "Type 'MyObject | boolean' does not have keys, so 'rawkeyof<MyObject | boolean>' is invalid");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_function_common_subset_if_union_of_differing_tables")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = { x: number, y: number, z: number }
type MyOtherObject = { w: number, y: number, z: number }
type KeysOfMyObject = rawkeyof<MyObject | MyOtherObject>
local function err(idx: KeysOfMyObject): "z" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"z\"", toString(tpm->wantedTp));
CHECK_EQ("\"y\" | \"z\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawkeyof_type_function_never_for_empty_table")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type RawkeyofEmpty = rawkeyof<{}>
local foo = ((nil :: any) :: RawkeyofEmpty)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("foo")) == "never");
}
TEST_CASE_FIXTURE(ClassFixture, "keyof_type_function_works_on_classes")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type KeysOfMyObject = keyof<BaseClass>
local function ok(idx: KeysOfMyObject): "BaseMethod" | "BaseField" | "Touched" return idx end
local function err(idx: KeysOfMyObject): "BaseMethod" return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("\"BaseMethod\"", toString(tpm->wantedTp));
CHECK_EQ("\"BaseField\" | \"BaseMethod\" | \"Touched\"", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(ClassFixture, "keyof_type_function_errors_if_it_has_nonclass_part")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type KeysOfMyObject = keyof<BaseClass | boolean>
local function err(idx: KeysOfMyObject): "BaseMethod" | "BaseField" return idx end
)");
// FIXME(CLI-95289): we should actually only report the type function being uninhabited error at its first use, I think?
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(toString(result.errors[0]) == "Type 'BaseClass | boolean' does not have keys, so 'keyof<BaseClass | boolean>' is invalid");
CHECK(toString(result.errors[1]) == "Type 'BaseClass | boolean' does not have keys, so 'keyof<BaseClass | boolean>' is invalid");
}
TEST_CASE_FIXTURE(ClassFixture, "keyof_type_function_common_subset_if_union_of_differing_classes")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type KeysOfMyObject = keyof<BaseClass | Vector2>
local function ok(idx: KeysOfMyObject): never return idx end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(ClassFixture, "keyof_type_function_works_with_parent_classes_too")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type KeysOfMyObject = keyof<ChildClass>
local function ok(idx: KeysOfMyObject): "BaseField" | "BaseMethod" | "Method" | "Touched" return idx end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(ClassFixture, "binary_type_function_works_with_default_argument")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type result = mul<number>
local function thunk(): result return 5 * 4 end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("() -> number" == toString(requireType("thunk")));
}
TEST_CASE_FIXTURE(ClassFixture, "vector2_multiply_is_overloaded")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local v = Vector2.New(1, 2)
local v2 = v * 1.5
local v3 = v * v
local v4 = v * "Hello" -- line 5
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(5 == result.errors[0].location.begin.line);
CHECK(5 == result.errors[0].location.end.line);
CHECK("Vector2" == toString(requireType("v2")));
CHECK("Vector2" == toString(requireType("v3")));
CHECK("mul<Vector2, string>" == toString(requireType("v4")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_rfc_example")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local animals = {
cat = { speak = function() print "meow" end },
dog = { speak = function() print "woof woof" end },
monkey = { speak = function() print "oo oo" end },
fox = { speak = function() print "gekk gekk" end }
}
type AnimalType = keyof<typeof(animals)>
function speakByType(animal: AnimalType)
animals[animal].speak()
end
speakByType("dog") -- ok
speakByType("cactus") -- errors
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm);
CHECK_EQ("\"cat\" | \"dog\" | \"fox\" | \"monkey\"", toString(tm->wantedType));
CHECK_EQ("\"cactus\"", toString(tm->givenType));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "keyof_oss_crash_gh1161")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local EnumVariants = {
["a"] = 1, ["b"] = 2, ["c"] = 3
}
type EnumKey = keyof<typeof(EnumVariants)>
function fnA<T>(i: T): keyof<T> end
function fnB(i: EnumKey) end
local result = fnA(EnumVariants)
fnB(result)
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(get<ConstraintSolvingIncompleteError>(result.errors[0]));
CHECK(get<FunctionExitsWithoutReturning>(result.errors[1]));
}
TEST_CASE_FIXTURE(TypeFunctionFixture, "fuzzer_numeric_binop_doesnt_assert_on_generalizeFreeType")
{
CheckResult result = check(R"(
Module 'l0':
local _ = (67108864)(_ >= _).insert
do end
do end
_(...,_(_,_(_()),_()))
(67108864)()()
_(_ ~= _ // _,l0)(_(_({n0,})),_(_),_)
_(setmetatable(_,{[...]=_,}))
)");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "cyclic_concat_function_at_work")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type T = concat<string | T, string>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireTypeAlias("T")) == "string");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "exceeded_distributivity_limits")
{
if (!FFlag::LuauSolverV2)
return;
ScopedFastInt sfi{DFInt::LuauTypeFamilyApplicationCartesianProductLimit, 10};
loadDefinition(R"(
declare class A
function __mul(self, rhs: unknown): A
end
declare class B
function __mul(self, rhs: unknown): B
end
declare class C
function __mul(self, rhs: unknown): C
end
declare class D
function __mul(self, rhs: unknown): D
end
)");
CheckResult result = check(R"(
type T = mul<A | B | C | D, A | B | C | D>
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(get<UninhabitedTypeFunction>(result.errors[0]));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "didnt_quite_exceed_distributivity_limits")
{
if (!FFlag::LuauSolverV2)
return;
// We duplicate the test here because we want to make sure the test failed
// due to exceeding the limits specifically, rather than any possible reasons.
ScopedFastInt sfi{DFInt::LuauTypeFamilyApplicationCartesianProductLimit, 20};
loadDefinition(R"(
declare class A
function __mul(self, rhs: unknown): A
end
declare class B
function __mul(self, rhs: unknown): B
end
declare class C
function __mul(self, rhs: unknown): C
end
declare class D
function __mul(self, rhs: unknown): D
end
)");
CheckResult result = check(R"(
type T = mul<A | B | C | D, A | B | C | D>
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "ensure_equivalence_with_distributivity")
{
if (!FFlag::LuauSolverV2)
return;
loadDefinition(R"(
declare class A
function __mul(self, rhs: unknown): A
end
declare class B
function __mul(self, rhs: unknown): B
end
declare class C
function __mul(self, rhs: unknown): C
end
declare class D
function __mul(self, rhs: unknown): D
end
)");
CheckResult result = check(R"(
type T = mul<A | B, C | D>
type U = mul<A, C> | mul<A, D> | mul<B, C> | mul<B, D>
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireTypeAlias("T")) == "A | B");
CHECK(toString(requireTypeAlias("U")) == "A | A | B | B");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "we_shouldnt_warn_that_a_reducible_type_function_is_uninhabited")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local Debounce = false
local Active = false
local function Use(Mode)
if Mode ~= nil then
if Mode == false and Active == false then
return
else
Active = not Mode
end
Debounce = false
end
Active = not Active
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = {a: string, b: number, c: boolean}
type IdxAType = index<MyObject, "a">
type IdxBType = index<MyObject, keyof<MyObject>>
local function ok(idx: IdxAType): string return idx end
local function ok2(idx: IdxBType): string | number | boolean return idx end
local function err(idx: IdxAType): boolean return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("boolean", toString(tpm->wantedTp));
CHECK_EQ("string", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_wait_for_pending_no_crash")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local PlayerData = {
Coins = 0,
Level = 1,
Exp = 0,
MaxExp = 100
}
type Keys = index<typeof(PlayerData), keyof<typeof(PlayerData)>>
-- This function makes it think that there's going to be a pending expansion
local function UpdateData(key: Keys, value)
PlayerData[key] = value
end
UpdateData("Coins", 2)
)");
// Should not crash!
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_array")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local MyObject = {"hello", 1, true}
type IdxAType = index<typeof(MyObject), number>
local function ok(idx: IdxAType): string | number | boolean return idx end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_generic_types")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local function access<T, K>(tbl: T & {}, key: K): index<T, K>
return tbl[key]
end
local subjects = {
english = "boring",
math = "fun"
}
local key: "english" = "english"
local a: string = access(subjects, key)
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_errors_w_bad_indexer")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = {a: string, b: number, c: boolean}
type errType1 = index<MyObject, "d">
type errType2 = index<MyObject, boolean>
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(toString(result.errors[0]) == "Property '\"d\"' does not exist on type 'MyObject'");
CHECK(toString(result.errors[1]) == "Property 'boolean' does not exist on type 'MyObject'");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_errors_w_var_indexer")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = {a: string, b: number, c: boolean}
local key = "a"
type errType1 = index<MyObject, key>
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(toString(result.errors[0]) == "Second argument to index<MyObject, _> is not a valid index type");
CHECK(toString(result.errors[1]) == "Unknown type 'key'");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_union_type_indexer")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = {a: string, b: number, c: boolean}
type idxType = index<MyObject, "a" | "b">
local function ok(idx: idxType): string | number return idx end
type errType = index<MyObject, "a" | "d">
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(result.errors[0]) == "Property '\"a\" | \"d\"' does not exist on type 'MyObject'");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_union_type_indexee")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = {a: string, b: number, c: boolean}
type MyObject2 = {a: number}
type idxTypeA = index<MyObject | MyObject2, "a">
local function ok(idx: idxTypeA): string | number return idx end
type errType = index<MyObject | MyObject2, "b">
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(result.errors[0]) == "Property '\"b\"' does not exist on type 'MyObject | MyObject2'");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_rfc_alternative_section")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = {a: string}
type MyObject2 = {a: string, b: number}
local function edgeCase(param: MyObject)
type unknownType = index<typeof(param), "b">
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(result.errors[0]) == "Property '\"b\"' does not exist on type 'MyObject'");
}
TEST_CASE_FIXTURE(ClassFixture, "index_type_function_works_on_classes")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type KeysOfMyObject = index<BaseClass, "BaseField">
local function ok(idx: KeysOfMyObject): number return idx end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(ClassFixture, "index_type_function_works_on_classes_with_parents")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type KeysOfMyObject = index<ChildClass, "BaseField">
local function ok(idx: KeysOfMyObject): number return idx end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "index_type_function_works_w_index_metatables")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local exampleClass = { Foo = "text", Bar = true }
local exampleClass2 = setmetatable({ Foo = 8 }, { __index = exampleClass })
type exampleTy2 = index<typeof(exampleClass2), "Foo">
local function ok(idx: exampleTy2): number return idx end
local exampleClass3 = setmetatable({ Bar = 5 }, { __index = exampleClass })
type exampleTy3 = index<typeof(exampleClass3), "Foo">
local function ok2(idx: exampleTy3): string return idx end
type exampleTy4 = index<typeof(exampleClass3), "Foo" | "Bar">
local function ok3(idx: exampleTy4): string | number return idx end
type errTy = index<typeof(exampleClass2), "Car">
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(result.errors[0]) == "Property '\"Car\"' does not exist on type 'exampleClass2'");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = {a: string, b: number, c: boolean}
type RawAType = rawget<MyObject, "a">
type RawBType = rawget<MyObject, keyof<MyObject>>
local function ok(idx: RawAType): string return idx end
local function ok2(idx: RawBType): string | number | boolean return idx end
local function err(idx: RawAType): boolean return idx end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
TypePackMismatch* tpm = get<TypePackMismatch>(result.errors[0]);
REQUIRE(tpm);
CHECK_EQ("boolean", toString(tpm->wantedTp));
CHECK_EQ("string", toString(tpm->givenTp));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works_w_array")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local MyObject = {"hello", 1, true}
type RawAType = rawget<typeof(MyObject), number>
local function ok(idx: RawAType): string | number | boolean return idx end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_errors_w_var_indexer")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = {a: string, b: number, c: boolean}
local key = "a"
type errType1 = rawget<MyObject, key>
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(toString(result.errors[0]) == "Second argument to rawget<MyObject, _> is not a valid index type");
CHECK(toString(result.errors[1]) == "Unknown type 'key'");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works_w_union_type_indexer")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = {a: string, b: number, c: boolean}
type rawType = rawget<MyObject, "a" | "b">
local function ok(idx: rawType): string | number return idx end
type errType = rawget<MyObject, "a" | "d">
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(result.errors[0]) == "Property '\"a\" | \"d\"' does not exist on type 'MyObject'");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works_w_union_type_indexee")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type MyObject = {a: string, b: number, c: boolean}
type MyObject2 = {a: number}
type rawTypeA = rawget<MyObject | MyObject2, "a">
local function ok(idx: rawTypeA): string | number return idx end
type errType = rawget<MyObject | MyObject2, "b">
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(result.errors[0]) == "Property '\"b\"' does not exist on type 'MyObject | MyObject2'");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "rawget_type_function_works_w_index_metatables")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
local exampleClass = { Foo = "text", Bar = true }
local exampleClass2 = setmetatable({ Foo = 8 }, { __index = exampleClass })
type exampleTy2 = rawget<typeof(exampleClass2), "Foo">
local function ok(idx: exampleTy2): number return idx end
local exampleClass3 = setmetatable({ Bar = 5 }, { __index = exampleClass })
type errType = rawget<typeof(exampleClass3), "Foo">
type errType2 = rawget<typeof(exampleClass3), "Bar" | "Foo">
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK(toString(result.errors[0]) == "Property '\"Foo\"' does not exist on type 'exampleClass3'");
CHECK(toString(result.errors[1]) == "Property '\"Bar\" | \"Foo\"' does not exist on type 'exampleClass3'");
}
TEST_CASE_FIXTURE(ClassFixture, "rawget_type_function_errors_w_classes")
{
if (!FFlag::LuauSolverV2)
return;
CheckResult result = check(R"(
type PropsOfMyObject = rawget<BaseClass, "BaseField">
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(toString(result.errors[0]) == "Property '\"BaseField\"' does not exist on type 'BaseClass'");
}
TEST_SUITE_END();