luau/tests/TypePath.test.cpp

540 lines
15 KiB
C++
Raw Normal View History

Sync to upstream/release/600 (#1076) ### What's Changed - Improve readability of unions and intersections by limiting the number of elements of those types that can be presented on a single line (gated under `FFlag::LuauToStringSimpleCompositeTypesSingleLine`) - Adds a new option to the compiler `--record-stats` to record and output compilation statistics - `if...then...else` expressions are now optimized into `AND/OR` form when possible. ### VM - Add a new `buffer` type to Luau based on the [buffer RFC](https://github.com/Roblox/luau/pull/739) and additional C API functions to work with it; this release does not include the library. - Internal C API to work with string buffers has been updated to align with Lua version more closely ### Native Codegen - Added support for new X64 instruction (rev) and new A64 instruction (bswap) in the assembler - Simplified the way numerical loop condition is translated to IR ### New Type Solver - Operator inference now handled by type families - Created a new system called `Type Paths` to explain why subtyping tests fail in order to improve the quality of error messages. - Systematic changes to implement Data Flow analysis in the new solver (`Breadcrumb` removed and replaced with `RefinementKey`) --- 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: Vighnesh Vijay <vvijay@roblox.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> --------- Co-authored-by: Arseny Kapoulkine <arseny.kapoulkine@gmail.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com> Co-authored-by: Andy Friesen <afriesen@roblox.com> Co-authored-by: Lily Brown <lbrown@roblox.com> Co-authored-by: Aaron Weiss <aaronweiss@roblox.com> Co-authored-by: Alexander McCord <amccord@roblox.com> Co-authored-by: Aviral Goel <agoel@roblox.com>
2023-10-21 09:10:30 +08:00
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/TypePath.h"
#include "Luau/Type.h"
#include "Luau/TypeArena.h"
#include "Luau/TypePack.h"
#include "ClassFixture.h"
#include "doctest.h"
#include "Fixture.h"
#include "ScopedFlags.h"
#include <optional>
using namespace Luau;
using namespace Luau::TypePath;
TEST_SUITE_BEGIN("TypePathManipulation");
TEST_CASE("append")
{
SUBCASE("empty_paths")
{
Path p;
CHECK(p.append(Path{}).empty());
}
SUBCASE("empty_path_with_path")
{
Path p1;
Path p2(TypeField::Metatable);
Path result = p1.append(p2);
CHECK(result == Path(TypeField::Metatable));
}
SUBCASE("two_paths")
{
Path p1(TypeField::IndexLookup);
Path p2(TypeField::Metatable);
Path result = p1.append(p2);
CHECK(result == Path({TypeField::IndexLookup, TypeField::Metatable}));
}
SUBCASE("all_components")
{
Path p1({TypeField::IndexLookup, TypeField::Metatable});
Path p2({TypeField::Metatable, PackField::Arguments});
Path result = p1.append(p2);
CHECK(result == Path({TypeField::IndexLookup, TypeField::Metatable, TypeField::Metatable, PackField::Arguments}));
}
SUBCASE("does_not_mutate")
{
Path p1(TypeField::IndexLookup);
Path p2(TypeField::Metatable);
p1.append(p2);
CHECK(p1 == Path(TypeField::IndexLookup));
CHECK(p2 == Path(TypeField::Metatable));
}
}
TEST_CASE("push")
{
Path p;
Path result = p.push(TypeField::Metatable);
CHECK(p.empty());
CHECK(result == Path(TypeField::Metatable));
}
TEST_CASE("pop")
{
SUBCASE("empty_path")
{
Path p;
CHECK(p.empty());
CHECK(p.pop().empty());
}
}
TEST_SUITE_END(); // TypePathManipulation
TEST_SUITE_BEGIN("TypePathTraversal");
#define TYPESOLVE_CODE(code) \
do \
{ \
CheckResult result = check(code); \
LUAU_REQUIRE_NO_ERRORS(result); \
} while (false);
TEST_CASE_FIXTURE(Fixture, "empty_traversal")
{
CHECK(traverseForType(builtinTypes->numberType, kEmpty, builtinTypes) == builtinTypes->numberType);
}
TEST_CASE_FIXTURE(Fixture, "table_property")
{
TYPESOLVE_CODE(R"(
local x = { y = 123 }
)");
CHECK(traverseForType(requireType("x"), Path(TypePath::Property{"y", true}), builtinTypes) == builtinTypes->numberType);
}
TEST_CASE_FIXTURE(ClassFixture, "class_property")
{
CHECK(traverseForType(vector2InstanceType, Path(TypePath::Property{"X", true}), builtinTypes) == builtinTypes->numberType);
}
TEST_CASE_FIXTURE(Fixture, "index")
{
SUBCASE("unions")
{
TYPESOLVE_CODE(R"(
type T = number | string | boolean
)");
SUBCASE("in_bounds")
{
CHECK(traverseForType(requireTypeAlias("T"), Path(TypePath::Index{1}), builtinTypes) == builtinTypes->stringType);
}
SUBCASE("out_of_bounds")
{
CHECK(traverseForType(requireTypeAlias("T"), Path(TypePath::Index{97}), builtinTypes) == std::nullopt);
}
}
SUBCASE("intersections")
{
// use functions to avoid the intersection being normalized away
TYPESOLVE_CODE(R"(
type T = (() -> ()) & ((true) -> false) & ((false) -> true)
)");
SUBCASE("in_bounds")
{
auto result = traverseForType(requireTypeAlias("T"), Path(TypePath::Index{1}), builtinTypes);
CHECK(result);
if (result)
CHECK(toString(*result) == "(true) -> false");
}
SUBCASE("out_of_bounds")
{
CHECK(traverseForType(requireTypeAlias("T"), Path(TypePath::Index{97}), builtinTypes) == std::nullopt);
}
}
SUBCASE("type_packs")
{
// use functions to avoid the intersection being normalized away
TYPESOLVE_CODE(R"(
type T = (number, string, true, false) -> ()
)");
SUBCASE("in_bounds")
{
Path path = Path({TypePath::PackField::Arguments, TypePath::Index{1}});
auto result = traverseForType(requireTypeAlias("T"), path, builtinTypes);
CHECK(result == builtinTypes->stringType);
}
SUBCASE("out_of_bounds")
{
Path path = Path({TypePath::PackField::Arguments, TypePath::Index{72}});
auto result = traverseForType(requireTypeAlias("T"), path, builtinTypes);
CHECK(result == std::nullopt);
}
}
}
TEST_CASE_FIXTURE(ClassFixture, "metatables")
{
SUBCASE("string")
{
auto result = traverseForType(builtinTypes->stringType, Path(TypeField::Metatable), builtinTypes);
CHECK(result == getMetatable(builtinTypes->stringType, builtinTypes));
}
SUBCASE("string_singleton")
{
TYPESOLVE_CODE(R"(
type T = "foo"
)");
auto result = traverseForType(requireTypeAlias("T"), Path(TypeField::Metatable), builtinTypes);
CHECK(result == getMetatable(builtinTypes->stringType, builtinTypes));
}
SUBCASE("table")
{
TYPESOLVE_CODE(R"(
local mt = { foo = 123 }
local tbl = setmetatable({}, mt)
)");
auto result = traverseForType(requireType("tbl"), Path(TypeField::Metatable), builtinTypes);
CHECK(result == requireType("mt"));
}
SUBCASE("class")
{
auto result = traverseForType(vector2InstanceType, Path(TypeField::Metatable), builtinTypes);
// ClassFixture's Vector2 metatable is just an empty table, but it's there.
CHECK(result);
}
}
TEST_CASE_FIXTURE(Fixture, "bounds")
{
SUBCASE("free_type")
{
TypeArena& arena = frontend.globals.globalTypes;
unfreeze(arena);
TypeId ty = arena.freshType(frontend.globals.globalScope.get());
FreeType* ft = getMutable<FreeType>(ty);
SUBCASE("upper")
{
ft->upperBound = builtinTypes->numberType;
auto result = traverseForType(ty, Path(TypeField::UpperBound), builtinTypes);
CHECK(result == builtinTypes->numberType);
}
SUBCASE("lower")
{
ft->lowerBound = builtinTypes->booleanType;
auto result = traverseForType(ty, Path(TypeField::LowerBound), builtinTypes);
CHECK(result == builtinTypes->booleanType);
}
}
SUBCASE("unbounded_type")
{
CHECK(traverseForType(builtinTypes->numberType, Path(TypeField::UpperBound), builtinTypes) == std::nullopt);
CHECK(traverseForType(builtinTypes->numberType, Path(TypeField::LowerBound), builtinTypes) == std::nullopt);
}
}
TEST_CASE_FIXTURE(Fixture, "indexers")
{
SUBCASE("table")
{
SUBCASE("lookup_indexer")
{
TYPESOLVE_CODE(R"(
type T = { [string]: boolean }
)");
auto lookupResult = traverseForType(requireTypeAlias("T"), Path(TypeField::IndexLookup), builtinTypes);
auto resultResult = traverseForType(requireTypeAlias("T"), Path(TypeField::IndexResult), builtinTypes);
CHECK(lookupResult == builtinTypes->stringType);
CHECK(resultResult == builtinTypes->booleanType);
}
SUBCASE("no_indexer")
{
TYPESOLVE_CODE(R"(
type T = { y: number }
)");
auto lookupResult = traverseForType(requireTypeAlias("T"), Path(TypeField::IndexLookup), builtinTypes);
auto resultResult = traverseForType(requireTypeAlias("T"), Path(TypeField::IndexResult), builtinTypes);
CHECK(lookupResult == std::nullopt);
CHECK(resultResult == std::nullopt);
}
}
// TODO: Class types
}
TEST_CASE_FIXTURE(Fixture, "negated")
{
SUBCASE("valid")
{
TypeArena& arena = frontend.globals.globalTypes;
unfreeze(arena);
TypeId ty = arena.addType(NegationType{builtinTypes->numberType});
auto result = traverseForType(ty, Path(TypeField::Negated), builtinTypes);
CHECK(result == builtinTypes->numberType);
}
SUBCASE("not_negation")
{
auto result = traverseForType(builtinTypes->numberType, Path(TypeField::Negated), builtinTypes);
CHECK(result == std::nullopt);
}
}
TEST_CASE_FIXTURE(Fixture, "variadic")
{
SUBCASE("valid")
{
TypeArena& arena = frontend.globals.globalTypes;
unfreeze(arena);
TypePackId tp = arena.addTypePack(VariadicTypePack{builtinTypes->numberType});
auto result = traverseForType(tp, Path(TypeField::Variadic), builtinTypes);
CHECK(result == builtinTypes->numberType);
}
SUBCASE("not_variadic")
{
auto result = traverseForType(builtinTypes->numberType, Path(TypeField::Variadic), builtinTypes);
CHECK(result == std::nullopt);
}
}
TEST_CASE_FIXTURE(Fixture, "arguments")
{
SUBCASE("function")
{
TYPESOLVE_CODE(R"(
function f(x: number, y: string)
end
)");
auto result = traverseForPack(requireType("f"), Path(PackField::Arguments), builtinTypes);
CHECK(result);
if (result)
CHECK(toString(*result) == "number, string");
}
SUBCASE("not_function")
{
auto result = traverseForPack(builtinTypes->booleanType, Path(PackField::Arguments), builtinTypes);
CHECK(result == std::nullopt);
}
}
TEST_CASE_FIXTURE(Fixture, "returns")
{
SUBCASE("function")
{
TYPESOLVE_CODE(R"(
function f(): (number, string)
return 123, "foo"
end
)");
auto result = traverseForPack(requireType("f"), Path(PackField::Returns), builtinTypes);
CHECK(result);
if (result)
CHECK(toString(*result) == "number, string");
}
SUBCASE("not_function")
{
auto result = traverseForPack(builtinTypes->booleanType, Path(PackField::Returns), builtinTypes);
CHECK(result == std::nullopt);
}
}
TEST_CASE_FIXTURE(Fixture, "tail")
{
SUBCASE("has_tail")
{
TYPESOLVE_CODE(R"(
type T = (number, string, ...boolean) -> ()
)");
auto result = traverseForPack(requireTypeAlias("T"), Path({PackField::Arguments, PackField::Tail}), builtinTypes);
CHECK(result);
if (result)
CHECK(toString(*result) == "...boolean");
}
SUBCASE("finite_pack")
{
TYPESOLVE_CODE(R"(
type T = (number, string) -> ()
)");
auto result = traverseForPack(requireTypeAlias("T"), Path({PackField::Arguments, PackField::Tail}), builtinTypes);
CHECK(result == std::nullopt);
}
SUBCASE("type")
{
auto result = traverseForPack(builtinTypes->stringType, Path({PackField::Arguments, PackField::Tail}), builtinTypes);
CHECK(result == std::nullopt);
}
}
TEST_CASE_FIXTURE(Fixture, "cycles" * doctest::timeout(0.5))
{
// This will fail an occurs check, but it's a quick example of a cyclic type
// where there _is_ no traversal.
SUBCASE("bound_cycle")
{
TypeArena& arena = frontend.globals.globalTypes;
unfreeze(arena);
TypeId a = arena.addType(BlockedType{});
TypeId b = arena.addType(BoundType{a});
asMutable(a)->ty.emplace<BoundType>(b);
CHECK_THROWS(traverseForType(a, Path(TypeField::IndexResult), builtinTypes));
}
SUBCASE("table_contains_itself")
{
TypeArena& arena = frontend.globals.globalTypes;
unfreeze(arena);
TypeId tbl = arena.addType(TableType{});
getMutable<TableType>(tbl)->props["a"] = Luau::Property(tbl);
auto result = traverseForType(tbl, Path(TypePath::Property{"a", true}), builtinTypes);
CHECK(result == tbl);
}
}
TEST_CASE_FIXTURE(Fixture, "step_limit")
{
ScopedFastInt sfi("LuauTypePathMaximumTraverseSteps", 2);
TYPESOLVE_CODE(R"(
type T = {
x: {
y: {
z: number
}
}
}
)");
TypeId root = requireTypeAlias("T");
Path path = PathBuilder().prop("x").prop("y").prop("z").build();
auto result = traverseForType(root, path, builtinTypes);
CHECK(!result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "complex_chains")
{
SUBCASE("add_metamethod_return_type")
{
TYPESOLVE_CODE(R"(
type Meta = {
__add: (Tab, Tab) -> number,
}
type Tab = typeof(setmetatable({}, {} :: Meta))
)");
TypeId root = requireTypeAlias("Tab");
Path path = PathBuilder().mt().prop("__add").rets().index(0).build();
auto result = traverseForType(root, path, builtinTypes);
CHECK(result == builtinTypes->numberType);
}
SUBCASE("overloaded_fn_overload_one_argument_two")
{
TYPESOLVE_CODE(R"(
type Obj = {
method: ((true, false) -> string) & ((string) -> number)
}
)");
TypeId root = requireTypeAlias("Obj");
Path path = PathBuilder().prop("method").index(0).args().index(1).build();
auto result = traverseForType(root, path, builtinTypes);
CHECK(*result == builtinTypes->falseType);
}
}
TEST_SUITE_END(); // TypePathTraversal
TEST_SUITE_BEGIN("TypePathToString");
TEST_CASE("field")
{
CHECK(toString(PathBuilder().prop("foo").build()) == R"(["foo"])");
}
TEST_CASE("index")
{
CHECK(toString(PathBuilder().index(0).build()) == "[0]");
}
TEST_CASE("chain")
{
CHECK(toString(PathBuilder().index(0).mt().build()) == "[0].metatable()");
}
TEST_SUITE_END(); // TypePathToString
TEST_SUITE_BEGIN("TypePathBuilder");
TEST_CASE("empty_path")
{
Path p = PathBuilder().build();
CHECK(p.empty());
}
TEST_CASE("prop")
{
Path p = PathBuilder().prop("foo").build();
CHECK(p == Path(TypePath::Property{"foo"}));
}
TEST_CASE("index")
{
Path p = PathBuilder().index(0).build();
CHECK(p == Path(TypePath::Index{0}));
}
TEST_CASE("fields")
{
CHECK(PathBuilder().mt().build() == Path(TypeField::Metatable));
CHECK(PathBuilder().lb().build() == Path(TypeField::LowerBound));
CHECK(PathBuilder().ub().build() == Path(TypeField::UpperBound));
CHECK(PathBuilder().indexKey().build() == Path(TypeField::IndexLookup));
CHECK(PathBuilder().indexValue().build() == Path(TypeField::IndexResult));
CHECK(PathBuilder().negated().build() == Path(TypeField::Negated));
CHECK(PathBuilder().variadic().build() == Path(TypeField::Variadic));
CHECK(PathBuilder().args().build() == Path(PackField::Arguments));
CHECK(PathBuilder().rets().build() == Path(PackField::Returns));
CHECK(PathBuilder().tail().build() == Path(PackField::Tail));
}
TEST_CASE("chained")
{
CHECK(PathBuilder().index(0).prop("foo").mt().prop("bar").args().index(1).build() ==
Path({Index{0}, TypePath::Property{"foo"}, TypeField::Metatable, TypePath::Property{"bar"}, PackField::Arguments, Index{1}}));
}
TEST_SUITE_END(); // TypePathBuilder