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>
906 lines
23 KiB
C++
906 lines
23 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
#include "Fixture.h"
|
|
|
|
#include "doctest.h"
|
|
|
|
LUAU_FASTFLAG(LuauSolverV2)
|
|
LUAU_FASTFLAG(LuauRefineWaitForBlockedTypesInTarget)
|
|
LUAU_FASTFLAG(LuauDoNotAddUpvalueTypesToLocalType)
|
|
LUAU_FASTFLAG(LuauDfgIfBlocksShouldRespectControlFlow)
|
|
LUAU_FASTFLAG(LuauReportSubtypingErrors)
|
|
LUAU_FASTFLAG(LuauEagerGeneralization)
|
|
LUAU_FASTFLAG(LuauPreprocessTypestatedArgument)
|
|
LUAU_FASTFLAG(LuauTableLiteralSubtypeSpecificCheck)
|
|
LUAU_FASTFLAG(LuauDfgAllowUpdatesInLoops)
|
|
LUAU_FASTFLAG(LuauSubtypeGenericsAndNegations)
|
|
LUAU_FASTFLAG(LuauNoMoreInjectiveTypeFunctions)
|
|
|
|
using namespace Luau;
|
|
|
|
namespace
|
|
{
|
|
struct TypeStateFixture : BuiltinsFixture
|
|
{
|
|
ScopedFastFlag dcr{FFlag::LuauSolverV2, true};
|
|
};
|
|
} // namespace
|
|
|
|
TEST_SUITE_BEGIN("TypeStatesTest");
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "initialize_x_of_type_string_or_nil_with_nil")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x: string? = nil
|
|
local a = x
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("string?" == toString(requireType("a")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "extraneous_lvalues_are_populated_with_nil")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(): (string, number)
|
|
return "hello", 5
|
|
end
|
|
|
|
local x, y, z = f()
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK("Function only returns 2 values, but 3 are required here" == toString(result.errors[0]));
|
|
CHECK("string" == toString(requireType("x")));
|
|
CHECK("number" == toString(requireType("y")));
|
|
CHECK("nil" == toString(requireType("z")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "assign_different_values_to_x")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x: string? = nil
|
|
local a = x
|
|
x = "hello!"
|
|
local b = x
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("string?" == toString(requireType("a")));
|
|
CHECK("string" == toString(requireType("b")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "parameter_x_was_constrained_by_two_types")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauTableLiteralSubtypeSpecificCheck, true};
|
|
|
|
// Parameter `x` has a fresh type `'x` bounded by `never` and `unknown`.
|
|
// The first use of `x` constrains `x`'s upper bound by `string | number`.
|
|
// The second use of `x`, aliased by `y`, constrains `x`'s upper bound by `string?`.
|
|
// This results in `'x <: (string | number) & (string?)`.
|
|
// The principal type of the upper bound is `string`.
|
|
CheckResult result = check(R"(
|
|
local function f(x): string?
|
|
local y: string | number = x
|
|
return y
|
|
end
|
|
)");
|
|
|
|
if (FFlag::LuauSolverV2)
|
|
{
|
|
// `y` is annotated `string | number` which is explicitly not compatible with `string?`
|
|
// as such, we produce an error here for that mismatch.
|
|
//
|
|
// this is not necessarily the best inference here, since we can indeed produce `string`
|
|
// as a type for `x`, but it's a limitation we can accept for now.
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
|
|
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
|
|
REQUIRE_MESSAGE(tm, "Expected TypeMismatch but got " << result.errors[0]);
|
|
CHECK("string?" == toString(tm->wantedType));
|
|
CHECK("number | string" == toString(tm->givenType));
|
|
CHECK("(number | string) -> string?" == toString(requireType("f")));
|
|
}
|
|
else
|
|
{
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK("(string) -> string?" == toString(requireType("f")));
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "local_that_will_be_assigned_later")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x: string
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "refine_a_local_and_then_assign_it")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: string?)
|
|
if typeof(x) == "string" then
|
|
x = nil
|
|
end
|
|
|
|
local y: nil = x
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "assign_a_local_and_then_refine_it")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: string?)
|
|
x = nil
|
|
|
|
if typeof(x) == "string" then
|
|
local y: typeof(x) = "hello"
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK("Type 'string' could not be converted into 'never'" == toString(result.errors[0]));
|
|
}
|
|
#endif
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "recursive_local_function")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x)
|
|
f(5)
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "recursive_function")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(x)
|
|
f(5)
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "compound_assignment")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x = 5
|
|
x += 7
|
|
|
|
local a = x
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "assignment_identity")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x = 5
|
|
x = x
|
|
|
|
local a = x
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("number" == toString(requireType("a")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "assignment_swap")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x, y = 5, "hello"
|
|
x, y = y, x
|
|
|
|
local a, b = x, y
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("string" == toString(requireType("a")));
|
|
CHECK("number" == toString(requireType("b")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "parameter_x_was_constrained_by_two_types_2")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x): number?
|
|
local y: string? = nil -- 'y <: string?
|
|
y = x -- 'y ~ 'x
|
|
return y -- 'y <: number?
|
|
|
|
-- We therefore infer 'y <: (string | nil) & (number | nil)
|
|
-- or 'y <: nil
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("(nil) -> number?" == toString(requireType("f")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "parameter_x_is_some_type_or_optional_then_assigned_with_alternate_value")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function f(x: number?)
|
|
x = x or 5
|
|
return x
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("(number?) -> number" == toString(requireType("f")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "local_assigned_in_either_branches_that_falls_through")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x = nil
|
|
if math.random() > 0.5 then
|
|
x = 5
|
|
else
|
|
x = "hello"
|
|
end
|
|
local y = x
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("number | string" == toString(requireType("y")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "local_assigned_in_only_one_branch_that_falls_through")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x = nil
|
|
if math.random() > 0.5 then
|
|
x = 5
|
|
end
|
|
local y = x
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("number?" == toString(requireType("y")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "then_branch_assigns_and_else_branch_also_assigns_but_is_met_with_return")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true};
|
|
|
|
CheckResult result = check(R"(
|
|
local x = nil
|
|
if math.random() > 0.5 then
|
|
x = 5
|
|
else
|
|
x = "hello"
|
|
return
|
|
end
|
|
local y = x
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("number" == toString(requireType("y")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "then_branch_assigns_but_is_met_with_return_and_else_branch_assigns")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true};
|
|
|
|
CheckResult result = check(R"(
|
|
local x = nil
|
|
if math.random() > 0.5 then
|
|
x = 5
|
|
return
|
|
else
|
|
x = "hello"
|
|
end
|
|
local y = x
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("string" == toString(requireType("y")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "invalidate_type_refinements_upon_assignments")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Ok<T> = { tag: "ok", val: T }
|
|
type Err<E> = { tag: "err", err: E }
|
|
type Result<T, E> = Ok<T> | Err<E>
|
|
|
|
local function f<T, E>(res: Result<T, E>)
|
|
assert(res.tag == "ok")
|
|
local tag: "ok", val: T = res.tag, res.val
|
|
res = { tag = "err" :: "err", err = (5 :: any) :: E }
|
|
local tag: "err", err: E = res.tag, res.err
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
#if 0
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "local_t_is_assigned_a_fresh_table_with_x_assigned_a_union_and_then_assert_restricts_actual_outflow_of_types")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local t = nil
|
|
|
|
if math.random() > 0.5 then
|
|
t = {}
|
|
t.x = if math.random() > 0.5 then 5 else "hello"
|
|
assert(typeof(t.x) == "string")
|
|
else
|
|
t = {}
|
|
t.x = if math.random() > 0.5 then 7 else true
|
|
assert(typeof(t.x) == "boolean")
|
|
end
|
|
|
|
local x = t.x
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
// CHECK("boolean | string" == toString(requireType("x")));
|
|
CHECK("boolean | number | number | string" == toString(requireType("x")));
|
|
}
|
|
#endif
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "captured_locals_do_not_mutate_upvalue_type")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true};
|
|
CheckResult result = check(R"(
|
|
local x = nil
|
|
|
|
function f()
|
|
print(x)
|
|
x = "five"
|
|
end
|
|
|
|
x = 5
|
|
f()
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
auto err = get<TypeMismatch>(result.errors[0]);
|
|
CHECK_EQ("number?", toString(err->wantedType));
|
|
CHECK_EQ("string", toString(err->givenType));
|
|
CHECK("number?" == toString(requireTypeAtPosition({4, 18})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "captured_locals_do_not_mutate_upvalue_type_2")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true};
|
|
CheckResult result = check(R"(
|
|
local t = {x = nil}
|
|
|
|
function f()
|
|
print(t.x)
|
|
t = {x = "five"}
|
|
end
|
|
|
|
t = {x = 5}
|
|
f()
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
auto err = get<TypeMismatch>(result.errors[0]);
|
|
CHECK_EQ("t | { x: number }", toString(err->wantedType));
|
|
CHECK_EQ("{ x: string }", toString(err->givenType));
|
|
|
|
CHECK("{ x: nil } | { x: number }" == toString(requireTypeAtPosition({4, 18}), {true}));
|
|
CHECK("number?" == toString(requireTypeAtPosition({4, 20})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "prototyped_recursive_functions")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local f
|
|
function f()
|
|
if math.random() > 0.5 then
|
|
f()
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("(() -> ())?" == toString(requireType("f")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "prototyped_recursive_functions_but_has_future_assignments")
|
|
{
|
|
ScopedFastFlag sffs[] = {
|
|
{FFlag::LuauSolverV2, true},
|
|
{FFlag::LuauReportSubtypingErrors, true},
|
|
{FFlag::LuauEagerGeneralization, true},
|
|
{FFlag::LuauSubtypeGenericsAndNegations, true},
|
|
{FFlag::LuauNoMoreInjectiveTypeFunctions, true},
|
|
};
|
|
|
|
CheckResult result = check(R"(
|
|
local f
|
|
function f()
|
|
if math.random() > 0.5 then
|
|
f()
|
|
end
|
|
end
|
|
f = 5
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
CHECK("((() -> ()) | number)?" == toString(requireType("f")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "prototyped_recursive_functions_but_has_previous_assignments")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local f
|
|
f = 5
|
|
function f()
|
|
if math.random() > 0.5 then
|
|
f()
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("((() -> ()) | number)?" == toString(requireType("f")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "multiple_assignments_in_loops")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local x = nil
|
|
|
|
for i = 1, 10 do
|
|
x = 5
|
|
x = "hello"
|
|
end
|
|
|
|
print(x)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("(number | string)?" == toString(requireType("x")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(TypeStateFixture, "typestates_preserve_error_suppression")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: any = 51
|
|
a = "pickles" -- We'll have a new DefId for this iteration of `a`. Its type must also be error-suppressing
|
|
print(a)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("*error-type* | string" == toString(requireTypeAtPosition({3, 14}), {true}));
|
|
}
|
|
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "typestates_preserve_error_suppression_properties")
|
|
{
|
|
// early return if the flag isn't set since this is blocking gated commits
|
|
// unconditional return
|
|
// CLI-117098 Type states with error suppressing properties doesn't infer the correct type for properties.
|
|
if (!FFlag::LuauSolverV2 || FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
local a: {x: any} = {x = 51}
|
|
a.x = "pickles" -- We'll have a new DefId for this iteration of `a.x`. Its type must also be error-suppressing
|
|
print(a.x)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("*error-type* | string" == toString(requireTypeAtPosition({3, 16}), {true}));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "typestates_do_not_apply_to_the_initial_local_definition")
|
|
{
|
|
// early return if the flag isn't set since this is blocking gated commits
|
|
if (!FFlag::LuauSolverV2)
|
|
return;
|
|
|
|
CheckResult result = check(R"(
|
|
type MyType = number | string
|
|
local foo: MyType = 5
|
|
print(foo)
|
|
foo = 7
|
|
print(foo)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK("number | string" == toString(requireTypeAtPosition({3, 14}), {true}));
|
|
CHECK("number" == toString(requireTypeAtPosition({5, 14}), {true}));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "typestate_globals")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
loadDefinition(R"(
|
|
declare foo: string | number
|
|
declare function f(x: string): ()
|
|
)");
|
|
|
|
CheckResult result = check(R"(
|
|
foo = "a"
|
|
f(foo)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "typestate_unknown_global")
|
|
{
|
|
ScopedFastFlag sff{FFlag::LuauSolverV2, true};
|
|
|
|
CheckResult result = check(R"(
|
|
x = 5
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
CHECK(get<UnknownSymbol>(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_normalized_type_variables_are_bad" * doctest::timeout(0.5))
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauRefineWaitForBlockedTypesInTarget, true};
|
|
// We do not care about the errors here, only that this finishes typing
|
|
// in a sensible amount of time.
|
|
LUAU_REQUIRE_ERRORS(check(R"(
|
|
local _
|
|
while _[""] do
|
|
_, _ = nil
|
|
while _.n0 do
|
|
_, _ = nil
|
|
end
|
|
_, _ = nil
|
|
end
|
|
while _[""] do
|
|
while if _ then if _ then _ else "" else "" do
|
|
_, _ = nil
|
|
do
|
|
end
|
|
_, _, _ = nil
|
|
end
|
|
_, _ = nil
|
|
_, _, _ = nil
|
|
while _.readi16 do
|
|
_, _ = nil
|
|
end
|
|
_, _ = nil
|
|
end
|
|
)"));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1547_simple")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true};
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(check(R"(
|
|
local rand = 0
|
|
|
|
function a()
|
|
rand = (rand % 4) + 1;
|
|
end
|
|
)"));
|
|
|
|
auto randTy = getType("rand");
|
|
REQUIRE(randTy);
|
|
CHECK_EQ("number", toString(*randTy));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "oss_1547")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true};
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(check(R"(
|
|
local rand = 0
|
|
|
|
function a()
|
|
rand = (rand % 4) + 1;
|
|
end
|
|
|
|
function b()
|
|
rand = math.max(rand - 1, 0);
|
|
end
|
|
)"));
|
|
|
|
auto randTy = getType("rand");
|
|
REQUIRE(randTy);
|
|
CHECK_EQ("number", toString(*randTy));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "modify_captured_table_field")
|
|
{
|
|
LUAU_REQUIRE_NO_ERRORS(check(R"(
|
|
local state = { x = 0 }
|
|
function incr()
|
|
state.x = state.x + 1
|
|
end
|
|
)"));
|
|
|
|
auto randTy = getType("state");
|
|
REQUIRE(randTy);
|
|
CHECK_EQ("{ x: number }", toString(*randTy, {true}));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "oss_1561")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true};
|
|
|
|
loadDefinition(R"(
|
|
declare class Vector3
|
|
X: number
|
|
Y: number
|
|
Z: number
|
|
end
|
|
|
|
declare Vector3: {
|
|
new: (number?, number?, number?) -> Vector3
|
|
}
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(check(R"(
|
|
local targetVelocity: Vector3 = Vector3.new()
|
|
function set2D(X: number, Y: number)
|
|
targetVelocity = Vector3.new(X, Y, targetVelocity.Z)
|
|
end
|
|
)"));
|
|
|
|
CHECK_EQ("(number, number) -> ()", toString(requireType("set2D")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "oss_1575")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true};
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(check(R"(
|
|
local flag = true
|
|
local function Flip()
|
|
flag = not flag
|
|
end
|
|
)"));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "capture_upvalue_in_returned_function")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDoNotAddUpvalueTypesToLocalType, true};
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(check(R"(
|
|
function def()
|
|
local i : number = 0
|
|
local function Counter()
|
|
i = i + 1
|
|
return i
|
|
end
|
|
return Counter
|
|
end
|
|
)"));
|
|
CHECK_EQ("() -> () -> number", toString(requireType("def")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "throw_in_else_branch")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true};
|
|
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
local x
|
|
local coinflip : () -> boolean = (nil :: any)
|
|
|
|
if coinflip () then
|
|
x = "I win."
|
|
else
|
|
error("You lose.")
|
|
end
|
|
|
|
print(x)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({11, 14})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "throw_in_if_branch")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true};
|
|
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
local x
|
|
local coinflip : () -> boolean = (nil :: any)
|
|
|
|
if coinflip () then
|
|
error("You lose.")
|
|
else
|
|
x = "I win."
|
|
end
|
|
|
|
print(x)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({11, 14})));
|
|
}
|
|
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refinement_through_erroring")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true};
|
|
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
type Payload = { payload: number }
|
|
|
|
local function decode(s: string): Payload?
|
|
return (nil :: any)
|
|
end
|
|
|
|
local function decodeEx(s: string): Payload
|
|
local p = decode(s)
|
|
if not p then
|
|
error("failed to decode payload!!!")
|
|
end
|
|
return p
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "refinement_through_erroring_in_loop")
|
|
{
|
|
ScopedFastFlag sffs[] = {
|
|
{FFlag::LuauSolverV2, true},
|
|
{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true},
|
|
{FFlag::LuauDfgAllowUpdatesInLoops,true}
|
|
};
|
|
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
|
|
local x = nil
|
|
|
|
while math.random() > 0.5 do
|
|
x = 42
|
|
return
|
|
end
|
|
|
|
print(x)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
CHECK_EQ("nil", toString(requireTypeAtPosition({10, 14})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "type_refinement_in_loop")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true};
|
|
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
local function onEachString(t: { string | number })
|
|
for _, v in t do
|
|
if type(v) ~= "string" then
|
|
continue
|
|
end
|
|
print(v)
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("number | string", toString(requireTypeAtPosition({4, 24})));
|
|
CHECK_EQ("string", toString(requireTypeAtPosition({7, 22})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "throw_in_if_branch_and_do_nothing_in_else")
|
|
{
|
|
ScopedFastFlag sffs[] = {
|
|
{FFlag::LuauSolverV2, true},
|
|
{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true},
|
|
};
|
|
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
local x
|
|
local coinflip : () -> boolean = (nil :: any)
|
|
|
|
if coinflip () then
|
|
error("You lose.")
|
|
else
|
|
end
|
|
|
|
print(x)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("nil", toString(requireTypeAtPosition({10, 14})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "assign_in_an_if_branch_without_else")
|
|
{
|
|
ScopedFastFlag sffs[] = {
|
|
{FFlag::LuauSolverV2, true},
|
|
{FFlag::LuauDfgIfBlocksShouldRespectControlFlow, true},
|
|
};
|
|
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
local x
|
|
local coinflip : () -> boolean = (nil :: any)
|
|
|
|
if coinflip () then
|
|
x = "I win."
|
|
end
|
|
|
|
print(x)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ("string?", toString(requireTypeAtPosition({9, 14})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_table_freeze_in_binary_expr")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauSolverV2, true};
|
|
// CLI-154237: This currently throws an exception due to a mismatch between
|
|
// the scopes created in the data flow graph versus the constraint generator.
|
|
CHECK_THROWS_AS(
|
|
check(R"(
|
|
local _
|
|
if _ or table.freeze(_,_) or table.freeze(_,_) then
|
|
end
|
|
)"),
|
|
Luau::InternalCompilerError
|
|
);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "table_freeze_in_conditional")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauSolverV2, true};
|
|
// NOTE: This _probably_ should be disallowed, but it is representing that
|
|
// type stating functions in short circuiting binary expressions do not
|
|
// reflect their type states.
|
|
CheckResult result = check(R"(
|
|
local t = { x = 42 }
|
|
if math.random() > 0.5 and table.freeze(t) then
|
|
end
|
|
t.y = 13
|
|
)");
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzzer_table_freeze_in_conditional_expr")
|
|
{
|
|
ScopedFastFlag _{FFlag::LuauSolverV2, true};
|
|
// CLI-154237: This currently throws an exception due to a mismatch between
|
|
// the scopes created in the data flow graph versus the constraint generator.
|
|
CHECK_THROWS_AS(
|
|
check(R"(
|
|
local _
|
|
if
|
|
if table.freeze(_,_) then _ else _
|
|
then
|
|
end
|
|
)"),
|
|
Luau::InternalCompilerError
|
|
);
|
|
}
|
|
|
|
TEST_SUITE_END();
|