mirror of
https://github.com/luau-lang/luau.git
synced 2025-01-05 19:09:11 +00:00
74c532053f
New Solver * New algorithm for inferring the types of locals that have no annotations. This algorithm is very conservative by default, but is augmented with some control flow awareness to handle most common scenarios. * Fix bugs in type inference of tables * Improve performance of by switching out standard C++ containers for `DenseHashMap` * Infrastructure to support clearer error messages in strict mode Native Code Generation * Fix a lowering issue with buffer.writeu8 and 0x80-0xff values: A constant argument wasn't truncated to the target type range and that causes an assertion failure in `build.mov`. * Store full lightuserdata value in loop iteration protocol lowering * Add analysis to compute function bytecode distribution * This includes a class to analyze the bytecode operator distribution per function and a CLI tool that produces a JSON report. See the new cmake target `Luau.Bytecode.CLI` --------- 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>
540 lines
14 KiB
C++
540 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"
|
|
#include "Luau/BuiltinDefinitions.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(a, b) end
|
|
local g : ((true, string) -> ()) & ((false, number) -> ()) = (f::any)
|
|
g(true, 37)
|
|
)");
|
|
|
|
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[] = {
|
|
{"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);
|
|
|
|
const std::string expectedError =
|
|
"Type 'a' could not be converted into 'Err<number> | Ok<string>'; type a (a) is not a subtype of Err<number> | Ok<string>[1] (Err<number>)\n"
|
|
"\ttype a[\"success\"] (false) is not exactly Err<number> | Ok<string>[0][\"success\"] (true)";
|
|
|
|
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);
|
|
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();
|