mirror of
https://github.com/luau-lang/luau.git
synced 2024-11-15 14:25:44 +08:00
f31232d301
# Old Solver: - Fix a bug in the old solver where a user could use the keyword `typeof` as the name of a type alias. - Fix stringification of scientific notation to omit a trailing decimal place when not followed by a digit e.g. `1.e+20` -> `1e+20` # New Solver - Continuing work on the New non-strict mode - Introduce `keyof` and `rawkeyof` type function for acquiring the type of all keys in a table or class (https://github.com/luau-lang/rfcs/pull/16) --- 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: Lily Brown <lbrown@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> Co-authored-by: Vighnesh Vijay <vvijay@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>
544 lines
14 KiB
C++
544 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();
|