mirror of
https://github.com/luau-lang/luau.git
synced 2025-08-26 11:27:08 +01:00
## General - Introduce `Frontend::parseModules` for parsing a group of modules at once. - Support chained function types in the CST. ## New Type Solver - Enable write-only table properties (described in [this RFC](https://rfcs.luau.org/property-writeonly.html)). - Disable singleton inference for large tables to improve performance. - Fix a bug that occurs when we try to expand a type alias to itself. - Catch cancelation during the type-checking phase in addition to during constraint solving. - Fix stringification of the empty type pack: `()`. - Improve errors for calls being rejected on the primitive `function` type. - Rework generalization: We now generalize types as soon as the last constraint relating to them is finished. We think this will reduce the number of cases where type inference fails to complete and reduce the number of instances where `*blocked*` types appear in the inference result. ## VM/Runtime - Dynamically disable native execution for functions that incur a slowdown (relative to bytecode execution). - Improve names for `thread`/`closure`/`proto` in the Luau heap dump. --- Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Ariel Weiss <aaronweiss@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Talha Pathan <tpathan@roblox.com> Co-authored-by: Varun Saini <vsaini@roblox.com> Co-authored-by: Vighnesh Vijay <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> --------- Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com> Co-authored-by: Alexander Youngblood <ayoungblood@roblox.com> Co-authored-by: Menarul Alam <malam@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: Vighnesh <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> Co-authored-by: Ariel Weiss <aaronweiss@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com>
890 lines
25 KiB
C++
890 lines
25 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
#include "Luau/BuiltinDefinitions.h"
|
|
#include "Luau/Common.h"
|
|
#include "Luau/Error.h"
|
|
#include "Luau/TypeInfer.h"
|
|
#include "Luau/Type.h"
|
|
|
|
#include "Fixture.h"
|
|
#include "ClassFixture.h"
|
|
|
|
#include "ScopedFlags.h"
|
|
#include "doctest.h"
|
|
|
|
using namespace Luau;
|
|
using std::nullopt;
|
|
|
|
LUAU_FASTFLAG(LuauSolverV2)
|
|
|
|
TEST_SUITE_BEGIN("TypeInferExternTypes");
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "Luau.Analyze.CLI_crashes_on_this_test")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local CircularQueue = {}
|
|
CircularQueue.__index = CircularQueue
|
|
|
|
function CircularQueue:new()
|
|
local newCircularQueue = {
|
|
head = nil,
|
|
}
|
|
setmetatable(newCircularQueue, CircularQueue)
|
|
|
|
return newCircularQueue
|
|
end
|
|
|
|
function CircularQueue:push()
|
|
local newListNode
|
|
|
|
if self.head then
|
|
newListNode = {
|
|
prevNode = self.head.prevNode,
|
|
nextNode = self.head,
|
|
}
|
|
newListNode.prevNode.nextNode = newListNode
|
|
newListNode.nextNode.prevNode = newListNode
|
|
end
|
|
end
|
|
|
|
return CircularQueue
|
|
|
|
)");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "call_method_of_a_class")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local m = BaseClass.StaticMethod()
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
REQUIRE_EQ("number", toString(requireType("m")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "call_method_of_a_child_class")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local m = ChildClass.StaticMethod()
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
REQUIRE_EQ("number", toString(requireType("m")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "call_instance_method")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local i = ChildClass.New()
|
|
local result = i:Method()
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("string", toString(requireType("result")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "call_base_method")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local i = ChildClass.New()
|
|
i:BaseMethod(41)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "cannot_call_unknown_method_of_a_class")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local m = BaseClass.Nope()
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "cannot_call_method_of_child_on_base_instance")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local i = BaseClass.New()
|
|
i:Method()
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "we_can_infer_that_a_parameter_must_be_a_particular_class")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function makeClone(o)
|
|
return BaseClass.Clone(o)
|
|
end
|
|
|
|
local a = makeClone(ChildClass.New())
|
|
)");
|
|
|
|
CHECK_EQ("BaseClass", toString(requireType("a")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function makeClone(o)
|
|
return BaseClass.Clone(o)
|
|
end
|
|
|
|
type Oopsies = { BaseMethod: (Oopsies, number) -> ()}
|
|
|
|
local oopsies: Oopsies = {
|
|
BaseMethod = function (self: Oopsies, i: number)
|
|
print('gadzooks!')
|
|
end
|
|
}
|
|
|
|
makeClone(oopsies)
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
TypeMismatch* tm = get<TypeMismatch>(result.errors.at(0));
|
|
REQUIRE(tm != nullptr);
|
|
|
|
CHECK_EQ("Oopsies", toString(tm->givenType));
|
|
CHECK_EQ("BaseClass", toString(tm->wantedType));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class_using_new_solver")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
CheckResult result = check(R"(
|
|
function makeClone(o)
|
|
return BaseClass.Clone(o)
|
|
end
|
|
|
|
type Oopsies = { read BaseMethod: (Oopsies, number) -> ()}
|
|
|
|
local oopsies: Oopsies = {
|
|
BaseMethod = function (self: Oopsies, i: number)
|
|
print('gadzooks!')
|
|
end
|
|
}
|
|
|
|
makeClone(oopsies)
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
TypeMismatch* tm = get<TypeMismatch>(result.errors.at(0));
|
|
REQUIRE(tm != nullptr);
|
|
|
|
CHECK_EQ("Oopsies", toString(tm->givenType));
|
|
CHECK_EQ("BaseClass", toString(tm->wantedType));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "assign_to_prop_of_class")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local v = Vector2.New(0, 5)
|
|
v.X = 55
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "can_read_prop_of_base_class")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local c = ChildClass.New()
|
|
local x = 1 + c.BaseField
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "can_assign_to_prop_of_base_class")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local c = ChildClass.New()
|
|
c.BaseField = 444
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "can_read_prop_of_base_class_using_string")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local c = ChildClass.New()
|
|
local x = 1 + c["BaseField"]
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "can_assign_to_prop_of_base_class_using_string")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local c = ChildClass.New()
|
|
c["BaseField"] = 444
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "cannot_unify_class_instance_with_primitive")
|
|
{
|
|
// This is allowed in the new solver
|
|
DOES_NOT_PASS_NEW_SOLVER_GUARD();
|
|
|
|
CheckResult result = check(R"(
|
|
local v = Vector2.New(0, 5)
|
|
v = 444
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "warn_when_prop_almost_matches")
|
|
{
|
|
CheckResult result = check(R"(
|
|
Vector2.new(0, 0)
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
auto err = get<UnknownPropButFoundLikeProp>(result.errors.at(0));
|
|
REQUIRE(err != nullptr);
|
|
|
|
REQUIRE_EQ(1, err->candidates.size());
|
|
CHECK_EQ("New", *err->candidates.begin());
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "extern_types_can_have_overloaded_operators")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a = Vector2.New(1, 2)
|
|
local b = Vector2.New(3, 4)
|
|
local c = a + b
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("Vector2", toString(requireType("c")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "extern_types_without_overloaded_operators_cannot_be_added")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a = BaseClass.New()
|
|
local b = BaseClass.New()
|
|
local c = a + b
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "function_arguments_are_covariant")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(b: BaseClass) end
|
|
|
|
f(ChildClass.New())
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "higher_order_function_arguments_are_contravariant")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function apply(f: (BaseClass) -> ())
|
|
f(ChildClass.New()) -- 2
|
|
end
|
|
|
|
apply(function (c: ChildClass) end) -- 5
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "higher_order_function_return_values_are_covariant")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function apply(f: () -> BaseClass)
|
|
return f()
|
|
end
|
|
|
|
apply(function ()
|
|
return ChildClass.New()
|
|
end)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "higher_order_function_return_type_is_not_contravariant")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function apply(f: () -> BaseClass)
|
|
return f()
|
|
end
|
|
|
|
apply(function ()
|
|
return ChildClass.New()
|
|
end)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "table_properties_are_invariant")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(a: {foo: BaseClass})
|
|
a.foo = AnotherChild.New()
|
|
end
|
|
|
|
local t: {foo: ChildClass}
|
|
f(t) -- line 6. Breaks soundness.
|
|
|
|
function g(t: {foo: ChildClass})
|
|
end
|
|
|
|
local t2: {foo: BaseClass} = {foo=BaseClass.New()}
|
|
t2.foo = AnotherChild.New()
|
|
g(t2) -- line 13. Breaks soundness
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK_EQ(6, result.errors.at(0).location.begin.line);
|
|
CHECK_EQ(13, result.errors[1].location.begin.line);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "table_indexers_are_invariant")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(a: {[number]: BaseClass})
|
|
a[1] = AnotherChild.New()
|
|
end
|
|
|
|
local t: {[number]: ChildClass}
|
|
f(t) -- line 6. Breaks soundness.
|
|
|
|
function g(t: {[number]: ChildClass})
|
|
end
|
|
|
|
local t2: {[number]: BaseClass} = {BaseClass.New()}
|
|
t2[1] = AnotherChild.New()
|
|
g(t2) -- line 13. Breaks soundness
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
CHECK_EQ(6, result.errors.at(0).location.begin.line);
|
|
CHECK_EQ(13, result.errors[1].location.begin.line);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "table_class_unification_reports_sane_errors_for_missing_properties")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function foo(bar)
|
|
bar.Y = 1 -- valid
|
|
bar.x = 2 -- invalid, wanted 'X'
|
|
bar.w = 2 -- invalid
|
|
end
|
|
|
|
local a: Vector2
|
|
foo(a)
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK("Type 'Vector2' could not be converted into '{ Y: number, w: number, x: number }'" == toString(result.errors[0]));
|
|
}
|
|
else
|
|
{
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
REQUIRE_EQ("Key 'w' not found in class 'Vector2'", toString(result.errors.at(0)));
|
|
REQUIRE_EQ("Key 'x' not found in class 'Vector2'. Did you mean 'X'?", toString(result.errors[1]));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "class_unification_type_mismatch_is_correct_order")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local p: BaseClass
|
|
local foo: number = p
|
|
local foo2: BaseClass = 1
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
|
|
REQUIRE_EQ("Type 'BaseClass' could not be converted into 'number'", toString(result.errors.at(0)));
|
|
REQUIRE_EQ("Type 'number' could not be converted into 'BaseClass'", toString(result.errors[1]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "optional_class_field_access_error")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local b: Vector2? = nil
|
|
local a = b.X + b.Z
|
|
|
|
b.X = 2 -- real Vector2.X is also read-only
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(4, result);
|
|
CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors.at(0)));
|
|
CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[1]));
|
|
CHECK_EQ("Key 'Z' not found in class 'Vector2'", toString(result.errors[2]));
|
|
CHECK_EQ("Value of type 'Vector2?' could be nil", toString(result.errors[3]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "detailed_class_unification_error")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function foo(v)
|
|
return v.X :: number + string.len(v.Y)
|
|
end
|
|
|
|
local a: Vector2
|
|
local b = foo
|
|
b(a)
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
CHECK("Type 'number' could not be converted into 'string'" == toString(result.errors.at(0)));
|
|
}
|
|
else
|
|
{
|
|
const std::string expected = R"(Type 'Vector2' could not be converted into '{- X: number, Y: string -}'
|
|
caused by:
|
|
Property 'Y' is not compatible.
|
|
Type 'number' could not be converted into 'string')";
|
|
|
|
CHECK_EQ(expected, toString(result.errors.at(0)));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "class_type_mismatch_with_name_conflict")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local i = ChildClass.New()
|
|
type ChildClass = { x: number }
|
|
local a: ChildClass = i
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ("Type 'ChildClass' from 'Test' could not be converted into 'ChildClass' from 'MainModule'", toString(result.errors.at(0)));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "intersections_of_unions_of_extern_types")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : (BaseClass | Vector2) & (ChildClass | AnotherChild)
|
|
local y : (ChildClass | AnotherChild)
|
|
x = y
|
|
y = x
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "unions_of_intersections_of_extern_types")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : (BaseClass & ChildClass) | (BaseClass & AnotherChild) | (BaseClass & Vector2)
|
|
local y : (ChildClass | AnotherChild)
|
|
x = y
|
|
y = x
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "index_instance_property")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function execute(object: BaseClass, name: string)
|
|
print(object[name])
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ("Attempting a dynamic property access on type 'BaseClass' is unsafe and may cause exceptions at runtime", toString(result.errors.at(0)));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "index_instance_property_nonstrict")
|
|
{
|
|
CheckResult result = check(R"(
|
|
--!nonstrict
|
|
|
|
local function execute(object: BaseClass, name: string)
|
|
print(object[name])
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "type_mismatch_invariance_required_for_error")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type A = { x: ChildClass }
|
|
type B = { x: BaseClass }
|
|
|
|
local a: A = { x = ChildClass.New() }
|
|
local b: B = a
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK(
|
|
"Type 'A' could not be converted into 'B'; \n"
|
|
"this is because accessing `x` results in `ChildClass` in the former type and `BaseClass` in the latter type, and `ChildClass` is not "
|
|
"exactly `BaseClass`" == toString(result.errors.at(0))
|
|
);
|
|
else
|
|
{
|
|
const std::string expected = R"(Type 'A' could not be converted into 'B'
|
|
caused by:
|
|
Property 'x' is not compatible.
|
|
Type 'ChildClass' could not be converted into 'BaseClass' in an invariant context)";
|
|
CHECK_EQ(expected, toString(result.errors.at(0)));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "optional_class_casts_work_in_new_solver")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
CheckResult result = check(R"(
|
|
type A = { x: ChildClass }
|
|
type B = { x: BaseClass }
|
|
|
|
local a = { x = ChildClass.New() } :: A
|
|
local opt_a = a :: A?
|
|
local b = { x = BaseClass.New() } :: B
|
|
local opt_b = b :: B?
|
|
local b_from_a = a :: B
|
|
local b_from_opt_a = opt_a :: B
|
|
local opt_b_from_a = a :: B?
|
|
local opt_b_from_opt_a = opt_a :: B?
|
|
local a_from_b = b :: A
|
|
local a_from_opt_b = opt_b :: A
|
|
local opt_a_from_b = b :: A?
|
|
local opt_a_from_opt_b = opt_b :: A?
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "callable_extern_types")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : CallableClass
|
|
local y = x("testing")
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK_EQ("number", toString(requireType("y")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "indexable_extern_types")
|
|
{
|
|
// Test reading from an index
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableClass
|
|
local y = x.stringKey
|
|
)");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableClass
|
|
local y = x["stringKey"]
|
|
)");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableClass
|
|
local str : string
|
|
local y = x[str] -- Index with a non-const string
|
|
)");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableClass
|
|
local y = x[7] -- Index with a numeric key
|
|
)");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
// Test writing to an index
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableClass
|
|
x.stringKey = 42
|
|
)");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableClass
|
|
x["stringKey"] = 42
|
|
)");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableClass
|
|
local str : string
|
|
x[str] = 42 -- Index with a non-const string
|
|
)");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableClass
|
|
x[1] = 42 -- Index with a numeric key
|
|
)");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
// Try to index the class using an invalid type for the key (key type is 'number | string'.)
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableClass
|
|
local y = x[true]
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK("Type 'boolean' could not be converted into 'number | string'" == toString(result.errors.at(0)));
|
|
else
|
|
CHECK_EQ(
|
|
toString(result.errors.at(0)),
|
|
"Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible"
|
|
);
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableClass
|
|
x[true] = 42
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK("Type 'boolean' could not be converted into 'number | string'" == toString(result.errors.at(0)));
|
|
else
|
|
CHECK_EQ(
|
|
toString(result.errors.at(0)),
|
|
"Type 'boolean' could not be converted into 'number | string'; none of the union options are compatible"
|
|
);
|
|
}
|
|
|
|
// Test type checking for the return type of the indexer (i.e. a number)
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableClass
|
|
x.key = "string value"
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
// Disabled for now. CLI-115686
|
|
}
|
|
else
|
|
CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'");
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableClass
|
|
local str : string = x.key
|
|
)");
|
|
CHECK_EQ(toString(result.errors.at(0)), "Type 'number' could not be converted into 'string'");
|
|
}
|
|
|
|
// Check that we string key are rejected if the indexer's key type is not compatible with string
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableNumericKeyClass
|
|
x.key = 1
|
|
)");
|
|
CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'");
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableNumericKeyClass
|
|
x["key"] = 1
|
|
)");
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'");
|
|
else
|
|
CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'");
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableNumericKeyClass
|
|
local str : string
|
|
x[str] = 1 -- Index with a non-const string
|
|
)");
|
|
CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'");
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableNumericKeyClass
|
|
local y = x.key
|
|
)");
|
|
CHECK_EQ(toString(result.errors.at(0)), "Key 'key' not found in class 'IndexableNumericKeyClass'");
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableNumericKeyClass
|
|
local y = x["key"]
|
|
)");
|
|
if (FFlag::LuauSolverV2)
|
|
CHECK(toString(result.errors.at(0)) == "Key 'key' not found in class 'IndexableNumericKeyClass'");
|
|
else
|
|
CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'");
|
|
}
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x : IndexableNumericKeyClass
|
|
local str : string
|
|
local y = x[str] -- Index with a non-const string
|
|
)");
|
|
CHECK_EQ(toString(result.errors.at(0)), "Type 'string' could not be converted into 'number'");
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "read_write_class_properties")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
TypeArena& arena = frontend.globals.globalTypes;
|
|
|
|
unfreeze(arena);
|
|
|
|
TypeId instanceType = arena.addType(ExternType{"Instance", {}, nullopt, nullopt, {}, {}, "Test", {}});
|
|
getMutable<ExternType>(instanceType)->props = {{"Parent", Property::rw(instanceType)}};
|
|
|
|
//
|
|
|
|
TypeId workspaceType = arena.addType(ExternType{"Workspace", {}, nullopt, nullopt, {}, {}, "Test", {}});
|
|
|
|
TypeId scriptType =
|
|
arena.addType(ExternType{"Script", {{"Parent", Property::rw(workspaceType, instanceType)}}, instanceType, nullopt, {}, {}, "Test", {}});
|
|
|
|
TypeId partType = arena.addType(ExternType{
|
|
"Part",
|
|
{{"BrickColor", Property::rw(builtinTypes->stringType)}, {"Parent", Property::rw(workspaceType, instanceType)}},
|
|
instanceType,
|
|
nullopt,
|
|
{},
|
|
{},
|
|
"Test",
|
|
{}
|
|
});
|
|
|
|
getMutable<ExternType>(workspaceType)->props = {{"Script", Property::readonly(scriptType)}, {"Part", Property::readonly(partType)}};
|
|
|
|
frontend.globals.globalScope->bindings[frontend.globals.globalNames.names->getOrAdd("script")] = Binding{scriptType};
|
|
|
|
freeze(arena);
|
|
|
|
CheckResult result = check(R"(
|
|
script.Parent.Part.BrickColor = 0xFFFFFF
|
|
script.Parent.Part.Parent = script
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
CHECK(Location{{1, 40}, {1, 48}} == result.errors[0].location);
|
|
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
|
|
REQUIRE(tm);
|
|
CHECK(builtinTypes->stringType == tm->wantedType);
|
|
CHECK(builtinTypes->numberType == tm->givenType);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "cannot_index_a_class_with_no_indexer")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a = BaseClass.New()
|
|
|
|
local c = a[1]
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
CHECK_MESSAGE(
|
|
get<DynamicPropertyLookupOnExternTypesUnsafe>(result.errors[0]), "Expected DynamicPropertyLookupOnExternTypesUnsafe but got " << result.errors[0]
|
|
);
|
|
|
|
CHECK(builtinTypes->errorType == requireType("c"));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ExternTypeFixture, "cyclic_tables_are_assumed_to_be_compatible_with_extern_types")
|
|
{
|
|
/*
|
|
* This is technically documenting a case where we are intentionally
|
|
* unsound.
|
|
*
|
|
* Our builtins are essentially defined like so:
|
|
*
|
|
* declare class BaseClass
|
|
* BaseField: number
|
|
* function BaseMethod(self, number): ()
|
|
* read Touched: Connection
|
|
* end
|
|
*
|
|
* declare class Connection
|
|
* Connect: (Connection, (BaseClass) -> ()) -> ()
|
|
* end
|
|
*
|
|
* The type we infer for `onTouch` is
|
|
*
|
|
* (t1) -> () where t1 = { read BaseField: unknown, read BaseMethod: (t1, number) -> () }
|
|
*
|
|
* In order to validate that onTouch can be passed to Connect, we must
|
|
* verify the following relation:
|
|
*
|
|
* BaseClass <: t1 where t1 = { read BaseField: unknown, read BaseMethod: (t1, number) -> () }
|
|
*
|
|
* However, the cycle between the table and the function gums up the works
|
|
* here and the worst thing is that it's perfectly reasonable in principle.
|
|
* Just from these types, we cannot see that BaseMethod will only be passed
|
|
* t1. Without that guarantee, BaseClass cannot be used as a subtype of t1.
|
|
*
|
|
* I think the theoretically-correct way to untangle this would be to infer
|
|
* t1 as a bounded existential type.
|
|
*
|
|
* For now, we have a subtyping has a rule that provisionally substitutes
|
|
* the table for the class type when performing the subtyping test. We
|
|
* essentially assume that, for all cyclic functions, that the table and the
|
|
* class are mutually subtypes of one another.
|
|
*
|
|
* For more information, read uses of Subtyping::substitutions.
|
|
*/
|
|
|
|
CheckResult result = check(R"(
|
|
local c = BaseClass.New()
|
|
|
|
function requiresNothing() end
|
|
|
|
function onTouch(other)
|
|
requiresNothing(other:BaseMethod(0))
|
|
print(other.BaseField)
|
|
end
|
|
|
|
c.Touched:Connect(onTouch)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_SUITE_END();
|