mirror of
https://github.com/luau-lang/luau.git
synced 2024-11-15 14:25:44 +08:00
3b0e93bec9
# What's changed? Add program argument passing to scripts run using the Luau REPL! You can now pass `--program-args` (or shorthand `-a`) to the REPL which will treat all remaining arguments as arguments to pass to executed scripts. These values can be accessed through variadic argument expansion. You can read these values like so: ``` local args = {...} -- gets you an array of all the arguments ``` For example if we run the following script like `luau test.lua -a test1 test2 test3`: ``` -- test.lua print(...) ``` you should get the output: ``` test1 test2 test3 ``` ### Native Code Generation * Improve A64 lowering for vector operations by using vector instructions * Fix lowering issue in IR value location tracking! - A developer reported a divergence between code run in the VM and Native Code Generation which we have now fixed ### New Type Solver * Apply substitution to type families, and emit new constraints to reduce those further * More progress on reducing comparison (`lt/le`)type families * Resolve two major sources of cyclic types in the new solver ### Miscellaneous * Turned internal compiler errors (ICE's) into warnings and errors ------- Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> --------- Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com> Co-authored-by: David Cope <dcope@roblox.com> Co-authored-by: Lily Brown <lbrown@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
543 lines
14 KiB
C++
543 lines
14 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"
|
|
|
|
using namespace Luau;
|
|
|
|
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
|
|
|
|
TEST_SUITE_BEGIN("TypeSingletons");
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "function_args_infer_singletons")
|
|
{
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
type Phase = "A" | "B" | "C"
|
|
local function f(e : Phase) : number
|
|
return 0
|
|
end
|
|
local e = f("B")
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "bool_singletons")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: true = true
|
|
local b: false = false
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "string_singletons")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: "foo" = "foo"
|
|
local b: "bar" = "bar"
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "bool_singletons_mismatch")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: true = false
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ("Type 'false' could not be converted into 'true'", toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "string_singletons_mismatch")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: "foo" = "bar"
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ("Type '\"bar\"' could not be converted into '\"foo\"'", toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "string_singletons_escape_chars")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: "\n" = "\000\r"
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ(R"(Type '"\000\r"' could not be converted into '"\n"')", toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "bool_singleton_subtype")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: true = true
|
|
local b: boolean = a
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "string_singleton_subtype")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: "foo" = "foo"
|
|
local b: string = a
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "string_singleton_subtype_multi_assignment")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: "foo" = "foo"
|
|
local b: string, c: number = a, 10
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "function_call_with_singletons")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(a: true, b: "foo") end
|
|
f(true, "foo")
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "function_call_with_singletons_mismatch")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(a: true, b: "foo") end
|
|
f(true, "bar")
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ("Type '\"bar\"' could not be converted into '\"foo\"'", toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "overloaded_function_call_with_singletons")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(a, b) end
|
|
local g : ((true, string) -> ()) & ((false, number) -> ()) = (f::any)
|
|
g(true, "foo")
|
|
g(false, 37)
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "overloaded_function_call_with_singletons_mismatch")
|
|
{
|
|
CheckResult result = check(R"(
|
|
function f(g: ((true, string) -> ()) & ((false, number) -> ()))
|
|
g(true, 37)
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
|
if (FFlag::DebugLuauDeferredConstraintResolution)
|
|
{
|
|
CHECK_EQ("None of the overloads for function that accept 2 arguments are compatible.", toString(result.errors[0]));
|
|
CHECK_EQ("Available overloads: (true, string) -> (); and (false, number) -> ()", toString(result.errors[1]));
|
|
}
|
|
else
|
|
{
|
|
CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0]));
|
|
CHECK_EQ("Other overloads are also not viable: (false, number) -> ()", toString(result.errors[1]));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "enums_using_singletons")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type MyEnum = "foo" | "bar" | "baz"
|
|
local a : MyEnum = "foo"
|
|
local b : MyEnum = "bar"
|
|
local c : MyEnum = "baz"
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "enums_using_singletons_mismatch")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type MyEnum = "foo" | "bar" | "baz"
|
|
local a : MyEnum = "bang"
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ("Type '\"bang\"' could not be converted into '\"bar\" | \"baz\" | \"foo\"'; none of the union options are compatible",
|
|
toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "enums_using_singletons_subtyping")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type MyEnum1 = "foo" | "bar"
|
|
type MyEnum2 = MyEnum1 | "baz"
|
|
local a : MyEnum1 = "foo"
|
|
local b : MyEnum2 = a
|
|
local c : string = b
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "tagged_unions_using_singletons")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Dog = { tag: "Dog", howls: boolean }
|
|
type Cat = { tag: "Cat", meows: boolean }
|
|
type Animal = Dog | Cat
|
|
local a : Dog = { tag = "Dog", howls = true }
|
|
local b : Animal = { tag = "Cat", meows = true }
|
|
local c : Animal = a
|
|
c = b
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "tagged_unions_using_singletons_mismatch")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Dog = { tag: "Dog", howls: boolean }
|
|
type Cat = { tag: "Cat", meows: boolean }
|
|
type Animal = Dog | Cat
|
|
local a : Animal = { tag = "Cat", howls = true }
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "tagged_unions_immutable_tag")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Dog = { tag: "Dog", howls: boolean }
|
|
type Cat = { tag: "Cat", meows: boolean }
|
|
type Animal = Dog | Cat
|
|
local a : Animal = { tag = "Cat", meows = true }
|
|
a.tag = "Dog"
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "table_has_a_boolean")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local t={a=1,b=false}
|
|
)");
|
|
|
|
CHECK("{ a: number, b: boolean }" == toString(requireType("t"), {true}));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "table_properties_singleton_strings")
|
|
{
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
type T = {
|
|
["foo"] : number,
|
|
["$$bar"] : string,
|
|
baz : boolean
|
|
}
|
|
local t: T = {
|
|
["foo"] = 37,
|
|
["$$bar"] = "hi",
|
|
baz = true
|
|
}
|
|
local a: number = t.foo
|
|
local b: string = t["$$bar"]
|
|
local c: boolean = t.baz
|
|
t.foo = 5
|
|
t["$$bar"] = "lo"
|
|
t.baz = false
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
TEST_CASE_FIXTURE(Fixture, "table_properties_singleton_strings_mismatch")
|
|
{
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
type T = {
|
|
["$$bar"] : string,
|
|
}
|
|
local t: T = {
|
|
["$$bar"] = "hi",
|
|
}
|
|
t["$$bar"] = 5
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ("Type 'number' could not be converted into 'string'", toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "table_properties_alias_or_parens_is_indexer")
|
|
{
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
type S = "bar"
|
|
type T = {
|
|
[("foo")] : number,
|
|
[S] : string,
|
|
}
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ("Cannot have more than one table indexer", toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes")
|
|
{
|
|
CheckResult result = check(R"(
|
|
--!strict
|
|
local x: { ["<>"] : number }
|
|
x = { ["\n"] = 5 }
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
CHECK_EQ(R"(Table type '{ ["\n"]: number }' not compatible with type '{| ["<>"]: number |}' because the former is missing field '<>')",
|
|
toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "error_detailed_tagged_union_mismatch_string")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Cat = { tag: 'cat', catfood: string }
|
|
type Dog = { tag: 'dog', dogfood: string }
|
|
type Animal = Cat | Dog
|
|
|
|
local a: Animal = { tag = 'cat', cafood = 'something' }
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
const std::string expected = R"(Type 'a' could not be converted into 'Cat | Dog'
|
|
caused by:
|
|
None of the union options are compatible. For example:
|
|
Table type 'a' not compatible with type 'Cat' because the former is missing field 'catfood')";
|
|
CHECK_EQ(expected, toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "error_detailed_tagged_union_mismatch_bool")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Good = { success: true, result: string }
|
|
type Bad = { success: false, error: string }
|
|
type Result = Good | Bad
|
|
|
|
local a: Result = { success = false, result = 'something' }
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
const std::string expected = R"(Type 'a' could not be converted into 'Bad | Good'
|
|
caused by:
|
|
None of the union options are compatible. For example:
|
|
Table type 'a' not compatible with type 'Bad' because the former is missing field 'error')";
|
|
CHECK_EQ(expected, toString(result.errors[0]));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias")
|
|
{
|
|
ScopedFastFlag sff[] = {
|
|
{FFlag::DebugLuauDeferredConstraintResolution, true},
|
|
};
|
|
CheckResult result = check(R"(
|
|
type Ok<T> = {success: true, result: T}
|
|
type Err<T> = {success: false, error: T}
|
|
type Result<O, E> = Ok<O> | Err<E>
|
|
|
|
local a : Result<string, number> = {success = false, result = "hotdogs"}
|
|
-- local b : Result<string, number> = {success = true, result = "hotdogs"}
|
|
)");
|
|
|
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
|
|
|
// FIXME: This could be improved by expanding the contents of `a`
|
|
const std::string expectedError = "Type 'a' could not be converted into 'Err<number> | Ok<string>'";
|
|
|
|
CHECK(toString(result.errors[0]) == expectedError);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "if_then_else_expression_singleton_options")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Cat = { tag: 'cat', catfood: string }
|
|
type Dog = { tag: 'dog', dogfood: string }
|
|
type Animal = Cat | Dog
|
|
|
|
local a: Animal = if true then { tag = 'cat', catfood = 'something' } else { tag = 'dog', dogfood = 'other' }
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "widen_the_supertype_if_it_is_free_and_subtype_has_singleton")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function foo(f, x)
|
|
if x == "hi" then
|
|
f(x)
|
|
f("foo")
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 18})));
|
|
// should be <a...>((string) -> a..., string) -> () but needs lower bounds calculation
|
|
CHECK_EQ("<a, b...>((string) -> (b...), a) -> ()", toString(requireType("foo")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "return_type_of_f_is_not_widened")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function foo(f, x): "hello"? -- anyone there?
|
|
return if x == "hi"
|
|
then f(x)
|
|
else nil
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 23})));
|
|
CHECK_EQ(R"(<a, b, c...>((string) -> (a, c...), b) -> "hello"?)", toString(requireType("foo")));
|
|
// CHECK_EQ(R"(<a, b...>((string) -> ("hello"?, b...), a) -> "hello"?)", toString(requireType("foo")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "widening_happens_almost_everywhere")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local foo: "foo" = "foo"
|
|
local copy = foo
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
if (FFlag::DebugLuauDeferredConstraintResolution)
|
|
CHECK_EQ(R"("foo")", toString(requireType("copy")));
|
|
else
|
|
CHECK_EQ("string", toString(requireType("copy")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "widening_happens_almost_everywhere_except_for_tables")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Cat = {tag: "Cat", meows: boolean}
|
|
type Dog = {tag: "Dog", barks: boolean}
|
|
type Animal = Cat | Dog
|
|
|
|
local function f(tag: "Cat" | "Dog"): Animal?
|
|
if tag == "Cat" then
|
|
local result = {tag = tag, meows = true}
|
|
return result
|
|
elseif tag == "Dog" then
|
|
local result = {tag = tag, barks = true}
|
|
return result
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "functions_are_not_to_be_widened")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local function foo(my_enum: "A" | "B") end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ(R"(("A" | "B") -> ())", toString(requireType("foo")));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "indexing_on_string_singletons")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: string = "hi"
|
|
if a == "hi" then
|
|
local x = a:byte()
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 22})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "indexing_on_union_of_string_singletons")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: string = "hi"
|
|
if a == "hi" or a == "bye" then
|
|
local x = a:byte()
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ(R"("bye" | "hi")", toString(requireTypeAtPosition({3, 22})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_string_singleton")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: string = "hi"
|
|
if a == "hi" then
|
|
local x = #a
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ(R"("hi")", toString(requireTypeAtPosition({3, 23})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "taking_the_length_of_union_of_string_singleton")
|
|
{
|
|
CheckResult result = check(R"(
|
|
local a: string = "hi"
|
|
if a == "hi" or a == "bye" then
|
|
local x = #a
|
|
end
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
|
|
CHECK_EQ(R"("bye" | "hi")", toString(requireTypeAtPosition({3, 23})));
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(Fixture, "no_widening_from_callsites")
|
|
{
|
|
CheckResult result = check(R"(
|
|
type Direction = "North" | "East" | "West" | "South"
|
|
|
|
local function direction(): Direction
|
|
return "North"
|
|
end
|
|
|
|
local d: Direction = direction()
|
|
)");
|
|
|
|
LUAU_REQUIRE_NO_ERRORS(result);
|
|
}
|
|
|
|
TEST_SUITE_END();
|