2021-10-29 21:25:12 +01:00
|
|
|
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
|
|
#include "Luau/TypeInfer.h"
|
2023-08-04 20:18:54 +01:00
|
|
|
#include "Luau/RecursionCounter.h"
|
2021-10-29 21:25:12 +01:00
|
|
|
|
|
|
|
#include "Fixture.h"
|
|
|
|
|
|
|
|
#include "doctest.h"
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
|
|
|
using namespace Luau;
|
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
LUAU_FASTFLAG(LuauSolverV2);
|
2023-12-02 07:46:57 +00:00
|
|
|
LUAU_FASTINT(LuauNormalizeCacheLimit);
|
|
|
|
LUAU_FASTINT(LuauTarjanChildLimit);
|
|
|
|
LUAU_FASTINT(LuauTypeInferIterationLimit);
|
|
|
|
LUAU_FASTINT(LuauTypeInferRecursionLimit);
|
|
|
|
LUAU_FASTINT(LuauTypeInferTypePackLoopLimit);
|
2023-07-28 16:13:53 +01:00
|
|
|
|
2021-10-29 21:25:12 +01:00
|
|
|
TEST_SUITE_BEGIN("ProvisionalTests");
|
|
|
|
|
2021-11-04 14:50:46 +00:00
|
|
|
// These tests check for behavior that differs from the final behavior we'd
|
2021-10-29 21:25:12 +01:00
|
|
|
// like to have. They serve to document the current state of the typechecker.
|
|
|
|
// When making future improvements, its very likely these tests will break and
|
|
|
|
// will need to be replaced.
|
|
|
|
|
|
|
|
/*
|
|
|
|
* This test falls into a sort of "do as I say" pit of consequences:
|
|
|
|
* Technically, the type of the type() function is <T>(T) -> string
|
|
|
|
*
|
|
|
|
* We thus infer that the argument to f is a free type.
|
|
|
|
* While we can still learn something about this argument, we can't seem to infer a union for it.
|
|
|
|
*
|
|
|
|
* Is this good? Maybe not, but I'm not sure what else we should do.
|
|
|
|
*/
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "typeguard_inference_incomplete")
|
|
|
|
{
|
|
|
|
const std::string code = R"(
|
|
|
|
function f(a)
|
|
|
|
if type(a) == "boolean" then
|
|
|
|
local a1 = a
|
|
|
|
elseif a.fn() then
|
|
|
|
local a2 = a
|
|
|
|
end
|
|
|
|
end
|
|
|
|
)";
|
|
|
|
|
2021-11-05 15:47:21 +00:00
|
|
|
const std::string expected = R"(
|
|
|
|
function f(a:{fn:()->(a,b...)}): ()
|
|
|
|
if type(a) == 'boolean'then
|
|
|
|
local a1:boolean=a
|
|
|
|
elseif a.fn()then
|
|
|
|
local a2:{fn:()->(a,b...)}=a
|
|
|
|
end
|
|
|
|
end
|
|
|
|
)";
|
|
|
|
|
2024-08-09 18:18:20 +01:00
|
|
|
const std::string expectedWithNewSolver = R"(
|
|
|
|
function f(a:{fn:()->(unknown,...unknown)}): ()
|
|
|
|
if type(a) == 'boolean'then
|
|
|
|
local a1:{fn:()->(unknown,...unknown)}&boolean=a
|
|
|
|
elseif a.fn()then
|
|
|
|
local a2:{fn:()->(unknown,...unknown)}&(class|function|nil|number|string|thread|buffer|table)=a
|
|
|
|
end
|
|
|
|
end
|
|
|
|
)";
|
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2024-08-09 18:18:20 +01:00
|
|
|
CHECK_EQ(expectedWithNewSolver, decorateWithTypes(code));
|
|
|
|
else
|
|
|
|
CHECK_EQ(expected, decorateWithTypes(code));
|
2021-10-29 21:25:12 +01:00
|
|
|
}
|
|
|
|
|
2023-05-05 22:52:49 +01:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "luau-polyfill.Array.filter")
|
|
|
|
{
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
2024-08-09 18:18:20 +01:00
|
|
|
|
2023-05-05 22:52:49 +01:00
|
|
|
// This test exercises the fact that we should reduce sealed/unsealed/free tables
|
|
|
|
// res is a unsealed table with type {((T & ~nil)?) & any}
|
|
|
|
// Because we do not reduce it fully, we cannot unify it with `Array<T> = { [number] : T}
|
|
|
|
// TLDR; reduction needs to reduce the indexer on res so it unifies with Array<T>
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
--!strict
|
|
|
|
-- Implements Javascript's `Array.prototype.filter` as defined below
|
|
|
|
-- https://developer.cmozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
|
|
|
|
type Array<T> = { [number]: T }
|
|
|
|
type callbackFn<T> = (element: T, index: number, array: Array<T>) -> boolean
|
|
|
|
type callbackFnWithThisArg<T, U> = (thisArg: U, element: T, index: number, array: Array<T>) -> boolean
|
|
|
|
type Object = { [string]: any }
|
|
|
|
return function<T, U>(t: Array<T>, callback: callbackFn<T> | callbackFnWithThisArg<T, U>, thisArg: U?): Array<T>
|
|
|
|
|
|
|
|
local len = #t
|
|
|
|
local res = {}
|
|
|
|
if thisArg == nil then
|
|
|
|
for i = 1, len do
|
|
|
|
local kValue = t[i]
|
|
|
|
if kValue ~= nil then
|
|
|
|
if (callback :: callbackFn<T>)(kValue, i, t) then
|
|
|
|
res[i] = kValue
|
|
|
|
end
|
|
|
|
end
|
|
|
|
end
|
|
|
|
else
|
|
|
|
end
|
|
|
|
|
|
|
|
return res
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
}
|
|
|
|
|
2022-05-13 20:36:37 +01:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "xpcall_returns_what_f_returns")
|
2021-10-29 21:25:12 +01:00
|
|
|
{
|
|
|
|
const std::string code = R"(
|
|
|
|
local a, b, c = xpcall(function() return 1, "foo" end, function() return "foo", 1 end)
|
|
|
|
)";
|
|
|
|
|
|
|
|
const std::string expected = R"(
|
|
|
|
local a:boolean,b:number,c:string=xpcall(function(): (number,string)return 1,'foo'end,function(): (string,number)return'foo',1 end)
|
|
|
|
)";
|
|
|
|
|
2022-09-02 00:14:03 +01:00
|
|
|
CheckResult result = check(code);
|
|
|
|
|
|
|
|
CHECK("boolean" == toString(requireType("a")));
|
|
|
|
CHECK("number" == toString(requireType("b")));
|
|
|
|
CHECK("string" == toString(requireType("c")));
|
|
|
|
|
|
|
|
CHECK(expected == decorateWithTypes(code));
|
2022-09-15 23:38:17 +01:00
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
2021-10-29 21:25:12 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// We had a bug where if you have two type packs that looks like:
|
|
|
|
// { x, y }, ...
|
|
|
|
// { x }, ...
|
|
|
|
// It would infinitely grow the type pack because one WeirdIter is trying to catch up, but can't.
|
|
|
|
// However, the following snippet is supposed to generate an OccursCheckFailed, but it doesn't.
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "weirditer_should_not_loop_forever")
|
|
|
|
{
|
|
|
|
// this flag is intentionally here doing nothing to demonstrate that we exit early via case detection
|
2023-12-02 07:46:57 +00:00
|
|
|
ScopedFastInt sfis{FInt::LuauTypeInferTypePackLoopLimit, 50};
|
2021-10-29 21:25:12 +01:00
|
|
|
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function toVertexList(vertices, x, y, ...)
|
|
|
|
if not (x and y) then return vertices end -- no more arguments
|
|
|
|
vertices[#vertices + 1] = {x = x, y = y} -- set vertex
|
|
|
|
return toVertexList(vertices, ...) -- recurse
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
}
|
|
|
|
|
|
|
|
// This should also generate an OccursCheckFailed error too, like the above toVertexList snippet.
|
|
|
|
// at least up until we can get Luau to recognize this code as a valid function that iterates over a list of values in the pack.
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "it_should_be_agnostic_of_actual_size")
|
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function f(x, y, ...)
|
|
|
|
if not y then return x end
|
|
|
|
return f(x, ...)
|
|
|
|
end
|
|
|
|
|
|
|
|
f(3, 2, 1, 0)
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Ideally setmetatable's second argument would be an optional free table.
|
|
|
|
// For now, infer it as just a free table.
|
2022-05-13 20:36:37 +01:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_constrains_free_type_into_free_table")
|
2021-10-29 21:25:12 +01:00
|
|
|
{
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
2024-08-09 18:18:20 +01:00
|
|
|
|
2021-10-29 21:25:12 +01:00
|
|
|
CheckResult result = check(R"(
|
|
|
|
local a = {}
|
|
|
|
local b
|
|
|
|
setmetatable(a, b)
|
|
|
|
b = 1
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
|
|
|
|
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
|
|
|
|
REQUIRE(tm);
|
|
|
|
CHECK_EQ("{- -}", toString(tm->wantedType));
|
|
|
|
CHECK_EQ("number", toString(tm->givenType));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Luau currently doesn't yet know how to allow assignments when the binding was refined.
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "while_body_are_also_refined")
|
|
|
|
{
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
2024-08-09 18:18:20 +01:00
|
|
|
|
2021-10-29 21:25:12 +01:00
|
|
|
CheckResult result = check(R"(
|
|
|
|
type Node<T> = { value: T, child: Node<T>? }
|
|
|
|
|
|
|
|
local function visitor<T>(node: Node<T>, f: (T) -> ())
|
|
|
|
local current = node
|
|
|
|
|
|
|
|
while current do
|
|
|
|
f(current.value)
|
|
|
|
current = current.child -- TODO: Can't work just yet. It thinks 'current' can never be nil. :(
|
|
|
|
end
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
|
|
|
|
CHECK_EQ("Type 'Node<T>?' could not be converted into 'Node<T>'", toString(result.errors[0]));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Originally from TypeInfer.test.cpp.
|
|
|
|
// I dont think type checking the metamethod at every site of == is the correct thing to do.
|
|
|
|
// We should be type checking the metamethod at the call site of setmetatable.
|
2022-05-13 20:36:37 +01:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "error_on_eq_metamethod_returning_a_type_other_than_boolean")
|
2021-10-29 21:25:12 +01:00
|
|
|
{
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
2024-08-09 18:18:20 +01:00
|
|
|
|
2021-10-29 21:25:12 +01:00
|
|
|
CheckResult result = check(R"(
|
|
|
|
local tab = {a = 1}
|
|
|
|
setmetatable(tab, {__eq = function(a, b): number
|
|
|
|
return 1
|
|
|
|
end})
|
|
|
|
local tab2 = tab
|
|
|
|
|
|
|
|
local a = tab2 == tab
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
|
|
|
|
GenericError* ge = get<GenericError>(result.errors[0]);
|
|
|
|
REQUIRE(ge);
|
|
|
|
CHECK_EQ("Metamethod '__eq' must return type 'boolean'", ge->message);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Belongs in TypeInfer.refinements.test.cpp.
|
2022-12-02 18:09:59 +00:00
|
|
|
// We need refine both operands as `never` in the `==` branch.
|
2021-10-29 21:25:12 +01:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "lvalue_equals_another_lvalue_with_no_overlap")
|
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function f(a: string, b: boolean?)
|
|
|
|
if a == b then
|
|
|
|
local foo, bar = a, b
|
|
|
|
else
|
|
|
|
local foo, bar = a, b
|
|
|
|
end
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
2022-12-02 18:09:59 +00:00
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
2021-10-29 21:25:12 +01:00
|
|
|
|
|
|
|
CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "string"); // a == b
|
|
|
|
CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "boolean?"); // a == b
|
|
|
|
|
|
|
|
CHECK_EQ(toString(requireTypeAtPosition({5, 33})), "string"); // a ~= b
|
|
|
|
CHECK_EQ(toString(requireTypeAtPosition({5, 36})), "boolean?"); // a ~= b
|
|
|
|
}
|
|
|
|
|
2022-01-27 23:46:05 +00:00
|
|
|
// Also belongs in TypeInfer.refinements.test.cpp.
|
|
|
|
// Just needs to fully support equality refinement. Which is annoying without type states.
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "discriminate_from_x_not_equal_to_nil")
|
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
type T = {x: string, y: number} | {x: nil, y: nil}
|
|
|
|
|
|
|
|
local function f(t: T)
|
|
|
|
if t.x ~= nil then
|
|
|
|
local foo = t
|
|
|
|
else
|
|
|
|
local bar = t
|
|
|
|
end
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2023-09-30 02:13:05 +01:00
|
|
|
{
|
|
|
|
CHECK_EQ("{ x: string, y: number }", toString(requireTypeAtPosition({5, 28})));
|
2024-08-09 18:18:20 +01:00
|
|
|
CHECK_EQ("{ x: nil, y: nil }", toString(requireTypeAtPosition({7, 28})));
|
2023-09-30 02:13:05 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
CHECK_EQ("{| x: string, y: number |}", toString(requireTypeAtPosition({5, 28})));
|
|
|
|
|
|
|
|
// Should be {| x: nil, y: nil |}
|
|
|
|
CHECK_EQ("{| x: nil, y: nil |} | {| x: string, y: number |}", toString(requireTypeAtPosition({7, 28})));
|
|
|
|
}
|
2022-01-27 23:46:05 +00:00
|
|
|
}
|
|
|
|
|
2022-07-08 02:22:39 +01:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "bail_early_if_unification_is_too_complicated" * doctest::timeout(0.5))
|
2021-10-29 21:25:12 +01:00
|
|
|
{
|
2023-12-02 07:46:57 +00:00
|
|
|
ScopedFastInt sffi{FInt::LuauTarjanChildLimit, 1};
|
|
|
|
ScopedFastInt sffi2{FInt::LuauTypeInferIterationLimit, 1};
|
2021-10-29 21:25:12 +01:00
|
|
|
|
|
|
|
CheckResult result = check(R"LUA(
|
|
|
|
local Result
|
|
|
|
Result = setmetatable({}, {})
|
|
|
|
Result.__index = Result
|
|
|
|
function Result.new(okValue)
|
|
|
|
local self = setmetatable({}, Result)
|
|
|
|
self:constructor(okValue)
|
|
|
|
return self
|
|
|
|
end
|
|
|
|
function Result:constructor(okValue)
|
|
|
|
self.okValue = okValue
|
|
|
|
end
|
|
|
|
function Result:ok(val) return Result.new(val) end
|
|
|
|
function Result:a(p0, p1, p2, p3, p4) return Result.new((self.okValue)) or p0 or p1 or p2 or p3 or p4 end
|
|
|
|
function Result:b(p0, p1, p2, p3, p4) return Result:ok((self.okValue)) or p0 or p1 or p2 or p3 or p4 end
|
|
|
|
function Result:c(p0, p1, p2, p3, p4) return Result:ok((self.okValue)) or p0 or p1 or p2 or p3 or p4 end
|
|
|
|
function Result:transpose(a)
|
|
|
|
return a and self.okValue:z(function(some)
|
|
|
|
return Result:ok(some)
|
|
|
|
end) or Result:ok(self.okValue)
|
|
|
|
end
|
|
|
|
)LUA");
|
|
|
|
|
2024-08-02 15:30:04 +01:00
|
|
|
auto it = std::find_if(
|
|
|
|
result.errors.begin(),
|
|
|
|
result.errors.end(),
|
|
|
|
[](TypeError& a)
|
|
|
|
{
|
|
|
|
return nullptr != get<UnificationTooComplex>(a);
|
|
|
|
}
|
|
|
|
);
|
2021-10-29 21:25:12 +01:00
|
|
|
if (it == result.errors.end())
|
|
|
|
{
|
|
|
|
dumpErrors(result);
|
|
|
|
FAIL("Expected a UnificationTooComplex error");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-11 19:02:09 +00:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "do_not_ice_when_trying_to_pick_first_of_generic_type_pack")
|
|
|
|
{
|
|
|
|
// In-place quantification causes these types to have the wrong types but only because of nasty interaction with prototyping.
|
|
|
|
// The type of f is initially () -> free1...
|
|
|
|
// Then the prototype iterator advances, and checks the function expression assigned to g, which has the type () -> free2...
|
|
|
|
// In the body it calls f and returns what f() returns. This binds free2... with free1..., causing f and g to have same types.
|
|
|
|
// We then quantify g, leaving it with the final type <a...>() -> a...
|
|
|
|
// Because free1... and free2... were bound, in combination with in-place quantification, f's return type was also turned into a...
|
|
|
|
// Then the check iterator catches up, and checks the body of f, and attempts to quantify it too.
|
|
|
|
// Alas, one of the requirements for quantification is that a type must contain free types. () -> a... has no free types.
|
|
|
|
// Thus the quantification for f was no-op, which explains why f does not have any type parameters.
|
|
|
|
// Calling f() will attempt to instantiate the function type, which turns generics in type binders into to free types.
|
|
|
|
// However, instantiations only converts generics contained within the type binders of a function, so instantiation was also no-op.
|
|
|
|
// Which means that calling f() simply returned a... rather than an instantiation of it. And since the call site was not in tail position,
|
|
|
|
// picking first element in a... triggers an ICE because calls returning generic packs are unexpected.
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function f() end
|
|
|
|
|
|
|
|
local g = function() return f() end
|
|
|
|
|
|
|
|
local x = (f()) -- should error: no return values to assign from the call to f
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2024-08-09 18:18:20 +01:00
|
|
|
{
|
|
|
|
CHECK("() -> ()" == toString(requireType("f")));
|
|
|
|
CHECK("() -> ()" == toString(requireType("g")));
|
|
|
|
CHECK("nil" == toString(requireType("x")));
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// f and g should have the type () -> ()
|
|
|
|
CHECK_EQ("() -> (a...)", toString(requireType("f")));
|
|
|
|
CHECK_EQ("<a...>() -> (a...)", toString(requireType("g")));
|
|
|
|
CHECK_EQ("any", toString(requireType("x"))); // any is returned instead of ICE for now
|
|
|
|
}
|
2022-02-11 19:02:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "specialization_binds_with_prototypes_too_early")
|
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function id(x) return x end
|
|
|
|
local n2n: (number) -> number = id
|
|
|
|
local s2s: (string) -> string = id
|
|
|
|
)");
|
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2024-08-09 18:18:20 +01:00
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
else
|
|
|
|
LUAU_REQUIRE_ERRORS(result); // Should not have any errors.
|
2022-02-11 19:02:09 +00:00
|
|
|
}
|
|
|
|
|
2022-07-21 22:16:54 +01:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "weird_fail_to_unify_type_pack")
|
|
|
|
{
|
|
|
|
ScopedFastFlag sff[] = {
|
2022-09-08 23:14:25 +01:00
|
|
|
// I'm not sure why this is broken without DCR, but it seems to be fixed
|
|
|
|
// when DCR is enabled.
|
2024-08-30 21:16:51 +01:00
|
|
|
{FFlag::LuauSolverV2, false},
|
2022-07-21 22:16:54 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function f() return end
|
|
|
|
local g = function() return f() end
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_ERRORS(result); // Should not have any errors.
|
|
|
|
}
|
|
|
|
|
2022-04-15 00:57:43 +01:00
|
|
|
// Belongs in TypeInfer.builtins.test.cpp.
|
2022-05-26 23:08:16 +01:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "pcall_returns_at_least_two_value_but_function_returns_nothing")
|
2022-04-15 00:57:43 +01:00
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function f(): () end
|
|
|
|
local ok, res = pcall(f)
|
|
|
|
)");
|
|
|
|
|
2022-05-26 23:08:16 +01:00
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
2022-09-29 23:23:10 +01:00
|
|
|
CHECK_EQ("Function only returns 1 value, but 2 are required here", toString(result.errors[0]));
|
2022-04-15 00:57:43 +01:00
|
|
|
// LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
// CHECK_EQ("boolean", toString(requireType("ok")));
|
|
|
|
// CHECK_EQ("any", toString(requireType("res")));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Belongs in TypeInfer.builtins.test.cpp.
|
2022-05-13 20:36:37 +01:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "choose_the_right_overload_for_pcall")
|
2022-04-15 00:57:43 +01:00
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function f(): number
|
|
|
|
if math.random() > 0.5 then
|
|
|
|
return 5
|
|
|
|
else
|
|
|
|
error("something")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local ok, res = pcall(f)
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("boolean", toString(requireType("ok")));
|
|
|
|
CHECK_EQ("number", toString(requireType("res")));
|
|
|
|
// CHECK_EQ("any", toString(requireType("res")));
|
|
|
|
}
|
|
|
|
|
|
|
|
// Belongs in TypeInfer.builtins.test.cpp.
|
2022-05-13 20:36:37 +01:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "function_returns_many_things_but_first_of_it_is_forgotten")
|
2022-04-15 00:57:43 +01:00
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function f(): (number, string, boolean)
|
|
|
|
if math.random() > 0.5 then
|
|
|
|
return 5, "hello", true
|
|
|
|
else
|
|
|
|
error("something")
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local ok, res, s, b = pcall(f)
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("boolean", toString(requireType("ok")));
|
|
|
|
CHECK_EQ("number", toString(requireType("res")));
|
|
|
|
// CHECK_EQ("any", toString(requireType("res")));
|
|
|
|
CHECK_EQ("string", toString(requireType("s")));
|
|
|
|
CHECK_EQ("boolean", toString(requireType("b")));
|
|
|
|
}
|
|
|
|
|
2022-07-08 02:22:39 +01:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "free_is_not_bound_to_any")
|
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function foo(f: (any) -> (), x)
|
|
|
|
f(x)
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
CHECK_EQ("((any) -> (), any) -> ()", toString(requireType("foo")));
|
|
|
|
}
|
|
|
|
|
2022-09-23 20:17:25 +01:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "dcr_can_partially_dispatch_a_constraint")
|
2022-08-25 22:53:50 +01:00
|
|
|
{
|
|
|
|
ScopedFastFlag sff[] = {
|
2024-08-30 21:16:51 +01:00
|
|
|
{FFlag::LuauSolverV2, true},
|
2022-08-25 22:53:50 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function hasDivisors(value: number)
|
|
|
|
end
|
|
|
|
|
|
|
|
function prime_iter(state, index)
|
|
|
|
hasDivisors(index)
|
|
|
|
index += 1
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
|
2022-11-10 22:53:13 +00:00
|
|
|
// Solving this requires recognizing that we can't dispatch a constraint
|
|
|
|
// like this without doing further work:
|
2022-08-25 22:53:50 +01:00
|
|
|
//
|
|
|
|
// (*blocked*) -> () <: (number) -> (b...)
|
|
|
|
//
|
2023-01-04 20:53:17 +00:00
|
|
|
// We solve this by searching both types for BlockedTypes and block the
|
2022-11-10 22:53:13 +00:00
|
|
|
// constraint on any we find. It also gets the job done, but I'm worried
|
|
|
|
// about the efficiency of doing so many deep type traversals and it may
|
|
|
|
// make us more prone to getting stuck on constraint cycles.
|
|
|
|
//
|
|
|
|
// If this doesn't pan out, a possible solution is to go further down the
|
|
|
|
// path of supporting partial constraint dispatch. The way it would work is
|
|
|
|
// that we'd dispatch the above constraint by binding b... to (), but we
|
|
|
|
// would append a new constraint number <: *blocked* to the constraint set
|
|
|
|
// to be solved later. This should be faster and theoretically less prone
|
|
|
|
// to cyclic constraint dependencies.
|
2023-09-22 20:12:15 +01:00
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2023-09-22 20:12:15 +01:00
|
|
|
CHECK("(unknown, number) -> ()" == toString(requireType("prime_iter")));
|
|
|
|
else
|
|
|
|
CHECK("<a>(a, number) -> ()" == toString(requireType("prime_iter")));
|
2022-08-25 22:53:50 +01:00
|
|
|
}
|
|
|
|
|
2023-06-02 20:52:15 +01:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "free_options_cannot_be_unified_together")
|
|
|
|
{
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
2024-08-09 18:18:20 +01:00
|
|
|
|
2023-06-02 20:52:15 +01:00
|
|
|
TypeArena arena;
|
|
|
|
TypeId nilType = builtinTypes->nilType;
|
|
|
|
|
|
|
|
std::unique_ptr scope = std::make_unique<Scope>(builtinTypes->anyTypePack);
|
|
|
|
|
|
|
|
TypeId free1 = arena.addType(FreeType{scope.get()});
|
|
|
|
TypeId option1 = arena.addType(UnionType{{nilType, free1}});
|
|
|
|
|
|
|
|
TypeId free2 = arena.addType(FreeType{scope.get()});
|
|
|
|
TypeId option2 = arena.addType(UnionType{{nilType, free2}});
|
|
|
|
|
|
|
|
InternalErrorReporter iceHandler;
|
|
|
|
UnifierSharedState sharedState{&iceHandler};
|
|
|
|
Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}};
|
|
|
|
Unifier u{NotNull{&normalizer}, NotNull{scope.get()}, Location{}, Variance::Covariant};
|
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2023-07-28 16:13:53 +01:00
|
|
|
u.enableNewSolver();
|
|
|
|
|
2023-06-02 20:52:15 +01:00
|
|
|
u.tryUnify(option1, option2);
|
|
|
|
|
|
|
|
CHECK(!u.failure);
|
|
|
|
|
|
|
|
u.log.commit();
|
|
|
|
|
|
|
|
ToStringOptions opts;
|
|
|
|
CHECK("a?" == toString(option1, opts));
|
|
|
|
|
|
|
|
// CHECK("a?" == toString(option2, opts)); // This should hold, but does not.
|
|
|
|
CHECK("b?" == toString(option2, opts)); // This should not hold.
|
|
|
|
}
|
|
|
|
|
2022-09-02 00:14:03 +01:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "for_in_loop_with_zero_iterators")
|
|
|
|
{
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
2022-09-02 00:14:03 +01:00
|
|
|
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
function no_iter() end
|
|
|
|
for key in no_iter() do end -- This should not be ok
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
}
|
|
|
|
|
2022-10-07 01:23:29 +01:00
|
|
|
// Ideally, we would not try to export a function type with generic types from incorrect scope
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "generic_type_leak_to_module_interface")
|
|
|
|
{
|
|
|
|
fileResolver.source["game/A"] = R"(
|
|
|
|
local wrapStrictTable
|
|
|
|
|
|
|
|
local metatable = {
|
|
|
|
__index = function(self, key)
|
|
|
|
local value = self.__tbl[key]
|
|
|
|
if type(value) == "table" then
|
|
|
|
-- unification of the free 'wrapStrictTable' with this function type causes generics of this function to leak out of scope
|
|
|
|
return wrapStrictTable(value, self.__name .. "." .. key)
|
|
|
|
end
|
|
|
|
return value
|
|
|
|
end,
|
|
|
|
}
|
|
|
|
|
|
|
|
return wrapStrictTable
|
|
|
|
)";
|
|
|
|
|
|
|
|
frontend.check("game/A");
|
|
|
|
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
|
|
local wrapStrictTable = require(game.A)
|
|
|
|
|
|
|
|
local Constants = {}
|
|
|
|
|
|
|
|
return wrapStrictTable(Constants, "Constants")
|
|
|
|
)";
|
|
|
|
|
|
|
|
frontend.check("game/B");
|
|
|
|
|
2023-04-21 23:14:26 +01:00
|
|
|
ModulePtr m = frontend.moduleResolver.getModule("game/B");
|
2022-10-07 01:23:29 +01:00
|
|
|
REQUIRE(m);
|
|
|
|
|
2023-01-06 21:14:35 +00:00
|
|
|
std::optional<TypeId> result = first(m->returnType);
|
2022-10-07 01:23:29 +01:00
|
|
|
REQUIRE(result);
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2024-08-09 18:18:20 +01:00
|
|
|
CHECK_EQ("unknown", toString(*result));
|
2023-03-17 19:20:37 +00:00
|
|
|
else
|
|
|
|
CHECK_MESSAGE(get<AnyType>(*result), *result);
|
2022-10-07 01:23:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "generic_type_leak_to_module_interface_variadic")
|
|
|
|
{
|
|
|
|
fileResolver.source["game/A"] = R"(
|
|
|
|
local wrapStrictTable
|
|
|
|
|
|
|
|
local metatable = {
|
|
|
|
__index = function<T>(self, key, ...: T)
|
|
|
|
local value = self.__tbl[key]
|
|
|
|
if type(value) == "table" then
|
|
|
|
-- unification of the free 'wrapStrictTable' with this function type causes generics of this function to leak out of scope
|
|
|
|
return wrapStrictTable(value, self.__name .. "." .. key)
|
|
|
|
end
|
|
|
|
return ...
|
|
|
|
end,
|
|
|
|
}
|
|
|
|
|
|
|
|
return wrapStrictTable
|
|
|
|
)";
|
|
|
|
|
|
|
|
frontend.check("game/A");
|
|
|
|
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
|
|
local wrapStrictTable = require(game.A)
|
|
|
|
|
|
|
|
local Constants = {}
|
|
|
|
|
|
|
|
return wrapStrictTable(Constants, "Constants")
|
|
|
|
)";
|
|
|
|
|
|
|
|
frontend.check("game/B");
|
|
|
|
|
2023-04-21 23:14:26 +01:00
|
|
|
ModulePtr m = frontend.moduleResolver.getModule("game/B");
|
2022-10-07 01:23:29 +01:00
|
|
|
REQUIRE(m);
|
|
|
|
|
2023-01-06 21:14:35 +00:00
|
|
|
std::optional<TypeId> result = first(m->returnType);
|
2022-10-07 01:23:29 +01:00
|
|
|
REQUIRE(result);
|
2024-08-09 18:18:20 +01:00
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2024-08-09 18:18:20 +01:00
|
|
|
CHECK("unknown" == toString(*result));
|
|
|
|
else
|
|
|
|
CHECK("any" == toString(*result));
|
2022-10-07 01:23:29 +01:00
|
|
|
}
|
|
|
|
|
2022-10-28 11:37:29 +01:00
|
|
|
namespace
|
|
|
|
{
|
|
|
|
struct IsSubtypeFixture : Fixture
|
2022-10-14 20:48:41 +01:00
|
|
|
{
|
|
|
|
bool isSubtype(TypeId a, TypeId b)
|
|
|
|
{
|
2023-01-06 21:14:35 +00:00
|
|
|
ModulePtr module = getMainModule();
|
|
|
|
REQUIRE(module);
|
|
|
|
|
|
|
|
if (!module->hasModuleScope())
|
|
|
|
FAIL("isSubtype: module scope data is not available");
|
|
|
|
|
|
|
|
return ::Luau::isSubtype(a, b, NotNull{module->getModuleScope().get()}, builtinTypes, ice);
|
2022-10-14 20:48:41 +01:00
|
|
|
}
|
|
|
|
};
|
2022-11-04 17:33:22 +00:00
|
|
|
} // namespace
|
2022-10-14 20:48:41 +01:00
|
|
|
|
2022-10-28 11:37:29 +01:00
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "intersection_of_functions_of_different_arities")
|
2022-10-14 20:48:41 +01:00
|
|
|
{
|
|
|
|
check(R"(
|
|
|
|
type A = (any) -> ()
|
|
|
|
type B = (any, any) -> ()
|
|
|
|
type T = A & B
|
|
|
|
|
|
|
|
local a: A
|
|
|
|
local b: B
|
|
|
|
local t: T
|
|
|
|
)");
|
|
|
|
|
|
|
|
[[maybe_unused]] TypeId a = requireType("a");
|
|
|
|
[[maybe_unused]] TypeId b = requireType("b");
|
|
|
|
|
|
|
|
// CHECK(!isSubtype(a, b)); // !!
|
|
|
|
// CHECK(!isSubtype(b, a));
|
|
|
|
|
|
|
|
CHECK("((any) -> ()) & ((any, any) -> ())" == toString(requireType("t")));
|
|
|
|
}
|
|
|
|
|
2022-10-28 11:37:29 +01:00
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "functions_with_mismatching_arity")
|
2022-10-14 20:48:41 +01:00
|
|
|
{
|
|
|
|
check(R"(
|
|
|
|
local a: (number) -> ()
|
|
|
|
local b: () -> ()
|
|
|
|
|
|
|
|
local c: () -> number
|
|
|
|
)");
|
|
|
|
|
|
|
|
TypeId a = requireType("a");
|
|
|
|
TypeId b = requireType("b");
|
|
|
|
TypeId c = requireType("c");
|
|
|
|
|
|
|
|
// CHECK(!isSubtype(b, a));
|
|
|
|
// CHECK(!isSubtype(c, a));
|
|
|
|
|
|
|
|
CHECK(!isSubtype(a, b));
|
|
|
|
// CHECK(!isSubtype(c, b));
|
|
|
|
|
|
|
|
CHECK(!isSubtype(a, c));
|
|
|
|
CHECK(!isSubtype(b, c));
|
|
|
|
}
|
|
|
|
|
2022-10-28 11:37:29 +01:00
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "functions_with_mismatching_arity_but_optional_parameters")
|
2022-10-14 20:48:41 +01:00
|
|
|
{
|
|
|
|
/*
|
|
|
|
* (T0..TN) <: (T0..TN, A?)
|
|
|
|
* (T0..TN) <: (T0..TN, any)
|
|
|
|
* (T0..TN, A?) </: (T0..TN) We don't technically need to spell this out, but it's quite important.
|
|
|
|
* T <: T
|
|
|
|
* if A <: B and B <: C then A <: C
|
|
|
|
* T -> R <: U -> S if U <: T and R <: S
|
|
|
|
* A | B <: T if A <: T and B <: T
|
|
|
|
* T <: A | B if T <: A or T <: B
|
|
|
|
*/
|
|
|
|
check(R"(
|
|
|
|
local a: (number?) -> ()
|
|
|
|
local b: (number) -> ()
|
|
|
|
local c: (number, number?) -> ()
|
|
|
|
)");
|
|
|
|
|
|
|
|
TypeId a = requireType("a");
|
|
|
|
TypeId b = requireType("b");
|
|
|
|
TypeId c = requireType("c");
|
|
|
|
|
|
|
|
/*
|
|
|
|
* (number) -> () </: (number?) -> ()
|
|
|
|
* because number? </: number (because number <: number, but nil </: number)
|
|
|
|
*/
|
|
|
|
CHECK(!isSubtype(b, a));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* (number, number?) </: (number?) -> ()
|
|
|
|
* because number? </: number (as above)
|
|
|
|
*/
|
|
|
|
CHECK(!isSubtype(c, a));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* (number?) -> () <: (number) -> ()
|
|
|
|
* because number <: number? (because number <: number)
|
|
|
|
*/
|
|
|
|
CHECK(isSubtype(a, b));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* (number, number?) -> () <: (number) -> (number)
|
|
|
|
* The packs have inequal lengths, but (number) <: (number, number?)
|
|
|
|
* and number <: number
|
|
|
|
*/
|
|
|
|
// CHECK(!isSubtype(c, b));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* (number?) -> () </: (number, number?) -> ()
|
|
|
|
* because (number, number?) </: (number)
|
|
|
|
*/
|
|
|
|
// CHECK(!isSubtype(a, c));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* (number) -> () </: (number, number?) -> ()
|
|
|
|
* because (number, number?) </: (number)
|
|
|
|
*/
|
|
|
|
// CHECK(!isSubtype(b, c));
|
|
|
|
}
|
|
|
|
|
2022-10-28 11:37:29 +01:00
|
|
|
TEST_CASE_FIXTURE(IsSubtypeFixture, "functions_with_mismatching_arity_but_any_is_an_optional_param")
|
2022-10-14 20:48:41 +01:00
|
|
|
{
|
|
|
|
check(R"(
|
|
|
|
local a: (number?) -> ()
|
|
|
|
local b: (number) -> ()
|
|
|
|
local c: (number, any) -> ()
|
|
|
|
)");
|
|
|
|
|
|
|
|
TypeId a = requireType("a");
|
|
|
|
TypeId b = requireType("b");
|
|
|
|
TypeId c = requireType("c");
|
|
|
|
|
|
|
|
/*
|
|
|
|
* (number) -> () </: (number?) -> ()
|
|
|
|
* because number? </: number (because number <: number, but nil </: number)
|
|
|
|
*/
|
|
|
|
CHECK(!isSubtype(b, a));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* (number, any) </: (number?) -> ()
|
|
|
|
* because number? </: number (as above)
|
|
|
|
*/
|
|
|
|
CHECK(!isSubtype(c, a));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* (number?) -> () <: (number) -> ()
|
|
|
|
* because number <: number? (because number <: number)
|
|
|
|
*/
|
|
|
|
CHECK(isSubtype(a, b));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* (number, any) -> () </: (number) -> (number)
|
|
|
|
* The packs have inequal lengths
|
|
|
|
*/
|
|
|
|
// CHECK(!isSubtype(c, b));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* (number?) -> () </: (number, any) -> ()
|
|
|
|
* The packs have inequal lengths
|
|
|
|
*/
|
|
|
|
// CHECK(!isSubtype(a, c));
|
|
|
|
|
|
|
|
/*
|
|
|
|
* (number) -> () </: (number, any) -> ()
|
|
|
|
* The packs have inequal lengths
|
|
|
|
*/
|
|
|
|
// CHECK(!isSubtype(b, c));
|
|
|
|
}
|
|
|
|
|
2022-12-09 19:57:01 +00:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_type_is_illegal")
|
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local t: {x: number?} = {x = nil}
|
|
|
|
|
|
|
|
if t.x then
|
|
|
|
local u: {x: number} = t
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2024-08-09 18:18:20 +01:00
|
|
|
LUAU_REQUIRE_NO_ERRORS(result); // This is wrong. We should be rejecting this assignment.
|
|
|
|
else
|
|
|
|
{
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
const std::string expected = R"(Type
|
2023-08-11 15:42:37 +01:00
|
|
|
'{| x: number? |}'
|
|
|
|
could not be converted into
|
|
|
|
'{| x: number |}'
|
2022-12-09 19:57:01 +00:00
|
|
|
caused by:
|
2023-10-13 21:20:12 +01:00
|
|
|
Property 'x' is not compatible.
|
2023-08-11 15:42:37 +01:00
|
|
|
Type 'number?' could not be converted into 'number' in an invariant context)";
|
2024-08-09 18:18:20 +01:00
|
|
|
CHECK_EQ(expected, toString(result.errors[0]));
|
|
|
|
}
|
2022-12-09 19:57:01 +00:00
|
|
|
}
|
|
|
|
|
Sync to upstream/release/562 (#828)
* Fixed rare use-after-free in analysis during table unification
A lot of work these past months went into two new Luau components:
* A near full rewrite of the typechecker using a new deferred constraint
resolution system
* Native code generation for AoT/JiT compilation of VM bytecode into x64
(avx)/arm64 instructions
Both of these components are far from finished and we don't provide
documentation on building and using them at this point.
However, curious community members expressed interest in learning about
changes that go into these components each week, so we are now listing
them here in the 'sync' pull request descriptions.
---
New typechecker can be enabled by setting
DebugLuauDeferredConstraintResolution flag to 'true'.
It is considered unstable right now, so try it at your own risk.
Even though it already provides better type inference than the current
one in some cases, our main goal right now is to reach feature parity
with current typechecker.
Features which improve over the capabilities of the current typechecker
are marked as '(NEW)'.
Changes to new typechecker:
* Regular for loop index and parameters are now typechecked
* Invalid type annotations on local variables are ignored to improve
autocomplete
* Fixed missing autocomplete type suggestions for function arguments
* Type reduction is now performed to produce simpler types to be
presented to the user (error messages, custom LSPs)
* Internally, complex types like '((number | string) & ~(false?)) |
string' can be produced, which is just 'string | number' when simplified
* Fixed spots where support for unknown and never types was missing
* (NEW) Length operator '#' is now valid to use on top table type, this
type comes up when doing typeof(x) == "table" guards and isn't available
in current typechecker
---
Changes to native code generation:
* Additional math library fast calls are now lowered to x64: math.ldexp,
math.round, math.frexp, math.modf, math.sign and math.clamp
2023-02-03 19:26:13 +00:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "table_insert_with_a_singleton_argument")
|
|
|
|
{
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
2024-08-09 18:18:20 +01:00
|
|
|
|
Sync to upstream/release/562 (#828)
* Fixed rare use-after-free in analysis during table unification
A lot of work these past months went into two new Luau components:
* A near full rewrite of the typechecker using a new deferred constraint
resolution system
* Native code generation for AoT/JiT compilation of VM bytecode into x64
(avx)/arm64 instructions
Both of these components are far from finished and we don't provide
documentation on building and using them at this point.
However, curious community members expressed interest in learning about
changes that go into these components each week, so we are now listing
them here in the 'sync' pull request descriptions.
---
New typechecker can be enabled by setting
DebugLuauDeferredConstraintResolution flag to 'true'.
It is considered unstable right now, so try it at your own risk.
Even though it already provides better type inference than the current
one in some cases, our main goal right now is to reach feature parity
with current typechecker.
Features which improve over the capabilities of the current typechecker
are marked as '(NEW)'.
Changes to new typechecker:
* Regular for loop index and parameters are now typechecked
* Invalid type annotations on local variables are ignored to improve
autocomplete
* Fixed missing autocomplete type suggestions for function arguments
* Type reduction is now performed to produce simpler types to be
presented to the user (error messages, custom LSPs)
* Internally, complex types like '((number | string) & ~(false?)) |
string' can be produced, which is just 'string | number' when simplified
* Fixed spots where support for unknown and never types was missing
* (NEW) Length operator '#' is now valid to use on top table type, this
type comes up when doing typeof(x) == "table" guards and isn't available
in current typechecker
---
Changes to native code generation:
* Additional math library fast calls are now lowered to x64: math.ldexp,
math.round, math.frexp, math.modf, math.sign and math.clamp
2023-02-03 19:26:13 +00:00
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function foo(t, x)
|
|
|
|
if x == "hi" or x == "bye" then
|
|
|
|
table.insert(t, x)
|
|
|
|
end
|
|
|
|
|
|
|
|
return t
|
|
|
|
end
|
|
|
|
|
|
|
|
local t = foo({}, "hi")
|
|
|
|
table.insert(t, "totally_unrelated_type" :: "totally_unrelated_type")
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
Sync to upstream/release/562 (#828)
* Fixed rare use-after-free in analysis during table unification
A lot of work these past months went into two new Luau components:
* A near full rewrite of the typechecker using a new deferred constraint
resolution system
* Native code generation for AoT/JiT compilation of VM bytecode into x64
(avx)/arm64 instructions
Both of these components are far from finished and we don't provide
documentation on building and using them at this point.
However, curious community members expressed interest in learning about
changes that go into these components each week, so we are now listing
them here in the 'sync' pull request descriptions.
---
New typechecker can be enabled by setting
DebugLuauDeferredConstraintResolution flag to 'true'.
It is considered unstable right now, so try it at your own risk.
Even though it already provides better type inference than the current
one in some cases, our main goal right now is to reach feature parity
with current typechecker.
Features which improve over the capabilities of the current typechecker
are marked as '(NEW)'.
Changes to new typechecker:
* Regular for loop index and parameters are now typechecked
* Invalid type annotations on local variables are ignored to improve
autocomplete
* Fixed missing autocomplete type suggestions for function arguments
* Type reduction is now performed to produce simpler types to be
presented to the user (error messages, custom LSPs)
* Internally, complex types like '((number | string) & ~(false?)) |
string' can be produced, which is just 'string | number' when simplified
* Fixed spots where support for unknown and never types was missing
* (NEW) Length operator '#' is now valid to use on top table type, this
type comes up when doing typeof(x) == "table" guards and isn't available
in current typechecker
---
Changes to native code generation:
* Additional math library fast calls are now lowered to x64: math.ldexp,
math.round, math.frexp, math.modf, math.sign and math.clamp
2023-02-03 19:26:13 +00:00
|
|
|
CHECK_EQ("{string}", toString(requireType("t")));
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// We'd really like for this to be {string}
|
|
|
|
CHECK_EQ("{string | string}", toString(requireType("t")));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-04-14 19:06:22 +01:00
|
|
|
// We really should be warning on this. We have no guarantee that T has any properties.
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "lookup_prop_of_intersection_containing_unions_of_tables_that_have_the_prop")
|
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local function mergeOptions<T>(options: T & ({variable: string} | {variable: number}))
|
|
|
|
return options.variable
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
|
|
|
|
// LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
|
|
|
|
// const UnknownProperty* unknownProp = get<UnknownProperty>(result.errors[0]);
|
|
|
|
// REQUIRE(unknownProp);
|
|
|
|
|
|
|
|
// CHECK("variable" == unknownProp->key);
|
|
|
|
}
|
|
|
|
|
2023-05-19 20:37:30 +01:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "expected_type_should_be_a_helpful_deduction_guide_for_function_calls")
|
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
type Ref<T> = { val: T }
|
|
|
|
|
|
|
|
local function useRef<T>(x: T): Ref<T?>
|
|
|
|
return { val = x }
|
|
|
|
end
|
|
|
|
|
|
|
|
local x: Ref<number?> = useRef(nil)
|
|
|
|
)");
|
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2024-08-09 18:18:20 +01:00
|
|
|
{
|
|
|
|
// This bug is fixed in the new solver.
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// This is actually wrong! Sort of. It's doing the wrong thing, it's actually asking whether
|
|
|
|
// `{| val: number? |} <: {| val: nil |}`
|
|
|
|
// instead of the correct way, which is
|
|
|
|
// `{| val: nil |} <: {| val: number? |}`
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
}
|
2023-05-19 20:37:30 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "floating_generics_should_not_be_allowed")
|
|
|
|
{
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
2024-08-09 18:18:20 +01:00
|
|
|
|
2023-05-19 20:37:30 +01:00
|
|
|
CheckResult result = check(R"(
|
|
|
|
local assign : <T, U, V, W>(target: T, source0: U?, source1: V?, source2: W?, ...any) -> T & U & V & W = (nil :: any)
|
|
|
|
|
|
|
|
-- We have a big problem here: The generics U, V, and W are not bound to anything!
|
|
|
|
-- Things get strange because of this.
|
|
|
|
local benchmark = assign({})
|
|
|
|
local options = benchmark.options
|
|
|
|
do
|
|
|
|
local resolve2: any = nil
|
|
|
|
options.fn({
|
|
|
|
resolve = function(...)
|
|
|
|
resolve2(...)
|
|
|
|
end,
|
|
|
|
})
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
}
|
|
|
|
|
2023-06-02 20:52:15 +01:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "free_options_can_be_unified_together")
|
|
|
|
{
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
2024-08-09 18:18:20 +01:00
|
|
|
|
2023-06-02 20:52:15 +01:00
|
|
|
TypeArena arena;
|
|
|
|
TypeId nilType = builtinTypes->nilType;
|
|
|
|
|
|
|
|
std::unique_ptr scope = std::make_unique<Scope>(builtinTypes->anyTypePack);
|
|
|
|
|
|
|
|
TypeId free1 = arena.addType(FreeType{scope.get()});
|
|
|
|
TypeId option1 = arena.addType(UnionType{{nilType, free1}});
|
|
|
|
|
|
|
|
TypeId free2 = arena.addType(FreeType{scope.get()});
|
|
|
|
TypeId option2 = arena.addType(UnionType{{nilType, free2}});
|
|
|
|
|
|
|
|
InternalErrorReporter iceHandler;
|
|
|
|
UnifierSharedState sharedState{&iceHandler};
|
|
|
|
Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}};
|
|
|
|
Unifier u{NotNull{&normalizer}, NotNull{scope.get()}, Location{}, Variance::Covariant};
|
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2023-07-28 16:13:53 +01:00
|
|
|
u.enableNewSolver();
|
|
|
|
|
2023-06-02 20:52:15 +01:00
|
|
|
u.tryUnify(option1, option2);
|
|
|
|
|
|
|
|
CHECK(!u.failure);
|
|
|
|
|
|
|
|
u.log.commit();
|
|
|
|
|
|
|
|
ToStringOptions opts;
|
|
|
|
CHECK("a?" == toString(option1, opts));
|
|
|
|
CHECK("b?" == toString(option2, opts)); // should be `a?`.
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "unify_more_complex_unions_that_include_nil")
|
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
type Record = {prop: (string | boolean)?}
|
|
|
|
|
|
|
|
function concatPagination(prop: (string | boolean | nil)?): Record
|
|
|
|
return {prop = prop}
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
}
|
|
|
|
|
2024-08-09 18:18:20 +01:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "optional_class_instances_are_invariant_old_solver")
|
2023-06-02 20:52:15 +01:00
|
|
|
{
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
2024-08-09 18:18:20 +01:00
|
|
|
|
2023-06-02 20:52:15 +01:00
|
|
|
createSomeClasses(&frontend);
|
|
|
|
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
function foo(ref: {current: Parent?})
|
|
|
|
end
|
|
|
|
|
|
|
|
function bar(ref: {current: Child?})
|
|
|
|
foo(ref)
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
}
|
|
|
|
|
2024-08-09 18:18:20 +01:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "optional_class_instances_are_invariant_new_solver")
|
|
|
|
{
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
2024-08-09 18:18:20 +01:00
|
|
|
|
|
|
|
createSomeClasses(&frontend);
|
|
|
|
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
function foo(ref: {read current: Parent?})
|
|
|
|
end
|
|
|
|
|
|
|
|
function bar(ref: {read current: Child?})
|
|
|
|
foo(ref)
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(0, result);
|
|
|
|
}
|
|
|
|
|
2023-06-02 20:52:15 +01:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "luau-polyfill.Map.entries")
|
|
|
|
{
|
|
|
|
|
|
|
|
fileResolver.source["Module/Map"] = R"(
|
|
|
|
--!strict
|
|
|
|
|
|
|
|
type Object = { [any]: any }
|
|
|
|
type Array<T> = { [number]: T }
|
|
|
|
type Table<T, V> = { [T]: V }
|
|
|
|
type Tuple<T, V> = Array<T | V>
|
|
|
|
|
|
|
|
local Map = {}
|
|
|
|
|
|
|
|
export type Map<K, V> = {
|
|
|
|
size: number,
|
|
|
|
-- method definitions
|
|
|
|
set: (self: Map<K, V>, K, V) -> Map<K, V>,
|
|
|
|
get: (self: Map<K, V>, K) -> V | nil,
|
|
|
|
clear: (self: Map<K, V>) -> (),
|
|
|
|
delete: (self: Map<K, V>, K) -> boolean,
|
|
|
|
has: (self: Map<K, V>, K) -> boolean,
|
|
|
|
keys: (self: Map<K, V>) -> Array<K>,
|
|
|
|
values: (self: Map<K, V>) -> Array<V>,
|
|
|
|
entries: (self: Map<K, V>) -> Array<Tuple<K, V>>,
|
|
|
|
ipairs: (self: Map<K, V>) -> any,
|
|
|
|
[K]: V,
|
|
|
|
_map: { [K]: V },
|
|
|
|
_array: { [number]: K },
|
|
|
|
}
|
|
|
|
|
|
|
|
function Map:entries()
|
|
|
|
return {}
|
|
|
|
end
|
|
|
|
|
|
|
|
local function coerceToTable(mapLike: Map<any, any> | Table<any, any>): Array<Tuple<any, any>>
|
|
|
|
local e = mapLike:entries();
|
|
|
|
return e
|
|
|
|
end
|
|
|
|
|
|
|
|
)";
|
|
|
|
|
|
|
|
CheckResult result = frontend.check("Module/Map");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
}
|
|
|
|
|
2023-08-04 20:18:54 +01:00
|
|
|
// We would prefer this unification to be able to complete, but at least it should not crash
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "table_unification_infinite_recursion")
|
|
|
|
{
|
2024-08-09 18:18:20 +01:00
|
|
|
// The new solver doesn't recurse as heavily in this situation.
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, false};
|
2024-08-09 18:18:20 +01:00
|
|
|
|
2023-08-04 20:18:54 +01:00
|
|
|
#if defined(_NOOPT) || defined(_DEBUG)
|
2023-12-02 07:46:57 +00:00
|
|
|
ScopedFastInt LuauTypeInferRecursionLimit{FInt::LuauTypeInferRecursionLimit, 100};
|
2023-08-04 20:18:54 +01:00
|
|
|
#endif
|
|
|
|
|
|
|
|
fileResolver.source["game/A"] = R"(
|
|
|
|
local tbl = {}
|
|
|
|
|
|
|
|
function tbl:f1(state)
|
|
|
|
self.someNonExistentvalue2 = state
|
|
|
|
end
|
|
|
|
|
|
|
|
function tbl:f2()
|
|
|
|
self.someNonExistentvalue:Dc()
|
|
|
|
end
|
|
|
|
|
|
|
|
function tbl:f3()
|
|
|
|
self:f2()
|
|
|
|
self:f1(false)
|
|
|
|
end
|
|
|
|
return tbl
|
|
|
|
)";
|
|
|
|
|
|
|
|
fileResolver.source["game/B"] = R"(
|
|
|
|
local tbl = require(game.A)
|
|
|
|
tbl:f3()
|
|
|
|
)";
|
|
|
|
|
2024-08-09 18:18:20 +01:00
|
|
|
CheckResult result = frontend.check("game/B");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
2023-08-04 20:18:54 +01:00
|
|
|
}
|
|
|
|
|
2023-09-01 18:58:27 +01:00
|
|
|
// Ideally, unification with any will not cause a 2^n normalization of a function overload
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "normalization_limit_in_unify_with_any")
|
|
|
|
{
|
|
|
|
ScopedFastFlag sff[] = {
|
2024-08-30 21:16:51 +01:00
|
|
|
{FFlag::LuauSolverV2, true},
|
2023-09-01 18:58:27 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
// With default limit, this test will take 10 seconds in NoOpt
|
2023-12-02 07:46:57 +00:00
|
|
|
ScopedFastInt luauNormalizeCacheLimit{FInt::LuauNormalizeCacheLimit, 1000};
|
2023-09-01 18:58:27 +01:00
|
|
|
|
|
|
|
// Build a function type with a large overload set
|
|
|
|
const int parts = 100;
|
|
|
|
std::string source;
|
|
|
|
|
|
|
|
for (int i = 0; i < parts; i++)
|
|
|
|
formatAppend(source, "type T%d = { f%d: number }\n", i, i);
|
|
|
|
|
|
|
|
source += "type Instance = { new: (('s0', extra: Instance?) -> T0)";
|
|
|
|
|
|
|
|
for (int i = 1; i < parts; i++)
|
|
|
|
formatAppend(source, " & (('s%d', extra: Instance?) -> T%d)", i, i);
|
|
|
|
|
|
|
|
source += " }\n";
|
|
|
|
|
|
|
|
source += R"(
|
|
|
|
local Instance: Instance = {} :: any
|
|
|
|
|
|
|
|
local function foo(a: typeof(Instance.new)) return if a then 2 else 3 end
|
|
|
|
|
|
|
|
foo(1 :: any)
|
|
|
|
)";
|
|
|
|
|
|
|
|
CheckResult result = check(source);
|
|
|
|
|
|
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
|
|
}
|
|
|
|
|
2024-04-05 21:45:09 +01:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "luau_roact_useState_nilable_state_1")
|
|
|
|
{
|
2024-08-30 21:16:51 +01:00
|
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
2024-04-05 21:45:09 +01:00
|
|
|
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
type Dispatch<A> = (A) -> ()
|
|
|
|
type BasicStateAction<S> = ((S) -> S) | S
|
|
|
|
|
|
|
|
type ScriptConnection = { Disconnect: (ScriptConnection) -> () }
|
|
|
|
|
|
|
|
local blah = nil :: any
|
|
|
|
|
|
|
|
local function useState<S>(
|
|
|
|
initialState: (() -> S) | S,
|
|
|
|
...
|
|
|
|
): (S, Dispatch<BasicStateAction<S>>)
|
|
|
|
return blah, blah
|
|
|
|
end
|
|
|
|
|
|
|
|
local a, b = useState(nil :: ScriptConnection?)
|
|
|
|
|
|
|
|
if a then
|
|
|
|
a:Disconnect()
|
|
|
|
b(nil :: ScriptConnection?)
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2024-04-05 21:45:09 +01:00
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
else
|
|
|
|
{
|
|
|
|
// This is a known bug in the old solver.
|
|
|
|
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
|
|
|
|
CHECK(Location{{19, 14}, {19, 41}} == result.errors[0].location);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-13 21:20:12 +01:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "luau_roact_useState_minimization")
|
|
|
|
{
|
|
|
|
// We don't expect this test to work on the old solver, but it also does not yet work on the new solver.
|
|
|
|
// So, we can't just put a scoped fast flag here, or it would block CI.
|
2024-08-30 21:16:51 +01:00
|
|
|
if (!FFlag::LuauSolverV2)
|
2023-10-13 21:20:12 +01:00
|
|
|
return;
|
|
|
|
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
type BasicStateAction<S> = ((S) -> S) | S
|
|
|
|
type Dispatch<A> = (A) -> ()
|
|
|
|
|
|
|
|
local function useState<S>(
|
|
|
|
initialState: (() -> S) | S
|
|
|
|
): (S, Dispatch<BasicStateAction<S>>)
|
|
|
|
-- fake impl that obeys types
|
|
|
|
local val = if type(initialState) == "function" then initialState() else initialState
|
|
|
|
return val, function(value)
|
|
|
|
return value
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
local test, setTest = useState(nil :: string?)
|
|
|
|
|
|
|
|
setTest(nil) -- this line causes the type to be narrowed in the old solver!!!
|
|
|
|
|
|
|
|
local function update(value: string)
|
|
|
|
print(test)
|
|
|
|
setTest(value)
|
|
|
|
end
|
|
|
|
|
|
|
|
update("hello")
|
|
|
|
)");
|
|
|
|
|
2024-08-09 18:18:20 +01:00
|
|
|
// We actually expect this code to be fine.
|
|
|
|
LUAU_REQUIRE_ERRORS(result);
|
2023-10-13 21:20:12 +01:00
|
|
|
}
|
|
|
|
|
2024-03-15 23:37:39 +00:00
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "bin_prov")
|
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local Bin = {}
|
|
|
|
|
|
|
|
function Bin:add(item)
|
|
|
|
self.head = { item = item}
|
|
|
|
return item
|
|
|
|
end
|
|
|
|
|
|
|
|
function Bin:destroy()
|
|
|
|
while self.head do
|
|
|
|
local item = self.head.item
|
|
|
|
if type(item) == "function" then
|
|
|
|
item()
|
|
|
|
elseif item.Destroy ~= nil then
|
|
|
|
end
|
|
|
|
self.head = self.head.next
|
|
|
|
end
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
}
|
|
|
|
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "update_phonemes_minimized")
|
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
local video
|
|
|
|
function(response)
|
|
|
|
for index = 1, #response do
|
|
|
|
video = video
|
|
|
|
end
|
|
|
|
return video
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
|
|
}
|
|
|
|
|
2024-04-12 18:18:49 +01:00
|
|
|
TEST_CASE_FIXTURE(Fixture, "table_containing_non_final_type_is_erroneously_cached")
|
|
|
|
{
|
|
|
|
TypeArena arena;
|
|
|
|
Scope globalScope(builtinTypes->anyTypePack);
|
|
|
|
UnifierSharedState sharedState{&ice};
|
|
|
|
Normalizer normalizer{&arena, builtinTypes, NotNull{&sharedState}};
|
|
|
|
|
|
|
|
TypeId tableTy = arena.addType(TableType{});
|
|
|
|
TableType* table = getMutable<TableType>(tableTy);
|
|
|
|
REQUIRE(table);
|
|
|
|
|
|
|
|
TypeId freeTy = arena.freshType(&globalScope);
|
|
|
|
|
|
|
|
table->props["foo"] = Property::rw(freeTy);
|
|
|
|
|
|
|
|
std::shared_ptr<const NormalizedType> n1 = normalizer.normalize(tableTy);
|
|
|
|
std::shared_ptr<const NormalizedType> n2 = normalizer.normalize(tableTy);
|
|
|
|
|
|
|
|
// This should not hold
|
|
|
|
CHECK(n1 == n2);
|
|
|
|
}
|
|
|
|
|
2024-06-21 00:37:55 +01:00
|
|
|
// This is doable with the new solver, but there are some problems we have to work out first.
|
|
|
|
// CLI-111113
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "we_cannot_infer_functions_that_return_inconsistently")
|
|
|
|
{
|
|
|
|
CheckResult result = check(R"(
|
|
|
|
function find_first<T>(tbl: {T}, el)
|
|
|
|
for i, e in tbl do
|
|
|
|
if e == el then
|
|
|
|
return i
|
|
|
|
end
|
|
|
|
end
|
|
|
|
return nil
|
|
|
|
end
|
|
|
|
)");
|
|
|
|
|
|
|
|
#if 0
|
|
|
|
// This #if block describes what should happen.
|
|
|
|
LUAU_CHECK_NO_ERRORS(result);
|
|
|
|
|
|
|
|
// The second argument has type unknown because the == operator does not
|
|
|
|
// constrain the type of el.
|
|
|
|
CHECK("<T>({T}, unknown) -> number?" == toString(requireType("find_first")));
|
|
|
|
#else
|
|
|
|
// This is what actually happens right now.
|
|
|
|
|
2024-08-30 21:16:51 +01:00
|
|
|
if (FFlag::LuauSolverV2)
|
2024-06-21 00:37:55 +01:00
|
|
|
{
|
|
|
|
LUAU_CHECK_ERROR_COUNT(2, result);
|
2024-09-13 19:48:25 +01:00
|
|
|
CHECK("<T>({T}, unknown) -> number" == toString(requireType("find_first")));
|
2024-06-21 00:37:55 +01:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
LUAU_CHECK_ERROR_COUNT(1, result);
|
|
|
|
|
|
|
|
CHECK("<T, b>({T}, b) -> number" == toString(requireType("find_first")));
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
|
2021-10-29 21:25:12 +01:00
|
|
|
TEST_SUITE_END();
|