luau/tests/Module.test.cpp

469 lines
14 KiB
C++
Raw Normal View History

// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
2022-04-08 05:29:01 +08:00
#include "Luau/Clone.h"
#include "Luau/Module.h"
#include "Luau/Scope.h"
2022-04-15 07:57:43 +08:00
#include "Luau/RecursionCounter.h"
Sync to upstream/release/572 (#899) * Fixed exported types not being suggested in autocomplete * `T...` is now convertible to `...any` (Fixes https://github.com/Roblox/luau/issues/767) * Fixed issue with `T?` not being convertible to `T | T` or `T?` (sometimes when internal pointer identity is different) * Fixed potential crash in missing table key error suggestion to use a similar existing key * `lua_topointer` now returns a pointer for strings C++ API Changes: * `prepareModuleScope` callback has moved from TypeChecker to Frontend * For LSPs, AstQuery functions (and `isWithinComment`) can be used without full Frontend data A lot of changes in our two experimental components as well. In our work on the new type-solver, the following issues were fixed: * Fixed table union and intersection indexing * Correct custom type environments are now used * Fixed issue with values of `free & number` type not accepted in numeric operations And these are the changes in native code generation (JIT): * arm64 lowering is almost complete with support for 99% of IR commands and all fastcalls * Fixed x64 assembly encoding for extended byte registers * More external x64 calls are aware of register allocator * `math.min`/`math.max` with more than 2 arguments are now lowered to IR as well * Fixed correctness issues with `math` library calls with multiple results in variadic context and with x64 register conflicts * x64 register allocator learnt to restore values from VM memory instead of always using stack spills * x64 exception unwind information now supports multiple functions and fixes function start offset in Dwarf2 info
2023-04-15 02:06:22 +08:00
#include "Luau/Parser.h"
#include "Fixture.h"
#include "doctest.h"
using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
2022-04-15 07:57:43 +08:00
TEST_SUITE_BEGIN("ModuleTests");
TEST_CASE_FIXTURE(Fixture, "is_within_comment")
{
check(R"(
--!strict
local foo = {}
function foo:bar() end
--[[
foo:
]] foo:bar()
--[[]]--[[]] -- Two distinct comments that have zero characters of space between them.
)");
SourceModule* sm = getMainSourceModule();
CHECK_EQ(5, sm->commentLocations.size());
CHECK(isWithinComment(*sm, Position{1, 15}));
CHECK(isWithinComment(*sm, Position{6, 16}));
CHECK(isWithinComment(*sm, Position{9, 13}));
CHECK(isWithinComment(*sm, Position{9, 14}));
CHECK(!isWithinComment(*sm, Position{2, 15}));
CHECK(!isWithinComment(*sm, Position{7, 10}));
CHECK(!isWithinComment(*sm, Position{7, 11}));
}
Sync to upstream/release/572 (#899) * Fixed exported types not being suggested in autocomplete * `T...` is now convertible to `...any` (Fixes https://github.com/Roblox/luau/issues/767) * Fixed issue with `T?` not being convertible to `T | T` or `T?` (sometimes when internal pointer identity is different) * Fixed potential crash in missing table key error suggestion to use a similar existing key * `lua_topointer` now returns a pointer for strings C++ API Changes: * `prepareModuleScope` callback has moved from TypeChecker to Frontend * For LSPs, AstQuery functions (and `isWithinComment`) can be used without full Frontend data A lot of changes in our two experimental components as well. In our work on the new type-solver, the following issues were fixed: * Fixed table union and intersection indexing * Correct custom type environments are now used * Fixed issue with values of `free & number` type not accepted in numeric operations And these are the changes in native code generation (JIT): * arm64 lowering is almost complete with support for 99% of IR commands and all fastcalls * Fixed x64 assembly encoding for extended byte registers * More external x64 calls are aware of register allocator * `math.min`/`math.max` with more than 2 arguments are now lowered to IR as well * Fixed correctness issues with `math` library calls with multiple results in variadic context and with x64 register conflicts * x64 register allocator learnt to restore values from VM memory instead of always using stack spills * x64 exception unwind information now supports multiple functions and fixes function start offset in Dwarf2 info
2023-04-15 02:06:22 +08:00
TEST_CASE_FIXTURE(Fixture, "is_within_comment_parse_result")
{
std::string src = R"(
--!strict
local foo = {}
function foo:bar() end
--[[
foo:
]] foo:bar()
--[[]]--[[]] -- Two distinct comments that have zero characters of space between them.
)";
Luau::Allocator alloc;
Luau::AstNameTable names{alloc};
Luau::ParseOptions parseOptions;
parseOptions.captureComments = true;
Luau::ParseResult parseResult = Luau::Parser::parse(src.data(), src.size(), names, alloc, parseOptions);
CHECK_EQ(5, parseResult.commentLocations.size());
CHECK(isWithinComment(parseResult, Position{1, 15}));
CHECK(isWithinComment(parseResult, Position{6, 16}));
CHECK(isWithinComment(parseResult, Position{9, 13}));
CHECK(isWithinComment(parseResult, Position{9, 14}));
CHECK(!isWithinComment(parseResult, Position{2, 15}));
CHECK(!isWithinComment(parseResult, Position{7, 10}));
CHECK(!isWithinComment(parseResult, Position{7, 11}));
}
TEST_CASE_FIXTURE(Fixture, "dont_clone_persistent_primitive")
{
TypeArena dest;
CloneState cloneState;
// numberType is persistent. We leave it as-is.
TypeId newNumber = clone(builtinTypes->numberType, dest, cloneState);
CHECK_EQ(newNumber, builtinTypes->numberType);
}
TEST_CASE_FIXTURE(Fixture, "deepClone_non_persistent_primitive")
{
TypeArena dest;
CloneState cloneState;
// Create a new number type that isn't persistent
unfreeze(frontend.globals.globalTypes);
TypeId oldNumber = frontend.globals.globalTypes.addType(PrimitiveType{PrimitiveType::Number});
freeze(frontend.globals.globalTypes);
2022-04-15 07:57:43 +08:00
TypeId newNumber = clone(oldNumber, dest, cloneState);
CHECK_NE(newNumber, oldNumber);
CHECK_EQ(*oldNumber, *newNumber);
CHECK_EQ("number", toString(newNumber));
CHECK_EQ(1, dest.types.size());
}
TEST_CASE_FIXTURE(Fixture, "deepClone_cyclic_table")
{
// Under DCR, we don't seal the outer occurrance of the table `Cyclic` which
// breaks this test. I'm not sure if that behaviour change is important or
// not, but it's tangental to the core purpose of this test.
ScopedFastFlag sff[] = {
{"DebugLuauDeferredConstraintResolution", false},
};
CheckResult result = check(R"(
local Cyclic = {}
function Cyclic.get()
return Cyclic
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
/* The inferred type of Cyclic is {get: () -> Cyclic}
*
* Assert that the return type of get() is the same as the outer table.
*/
TypeId ty = requireType("Cyclic");
TypeArena dest;
2022-04-15 07:57:43 +08:00
CloneState cloneState;
TypeId cloneTy = clone(ty, dest, cloneState);
TableType* ttv = getMutable<TableType>(cloneTy);
REQUIRE(ttv != nullptr);
CHECK_EQ(std::optional<std::string>{"Cyclic"}, ttv->syntheticName);
TypeId methodType = ttv->props["get"].type();
REQUIRE(methodType != nullptr);
const FunctionType* ftv = get<FunctionType>(methodType);
REQUIRE(ftv != nullptr);
2022-06-17 09:05:14 +08:00
std::optional<TypeId> methodReturnType = first(ftv->retTypes);
REQUIRE(methodReturnType);
CHECK_MESSAGE(methodReturnType == cloneTy, toString(methodType, {true}) << " should be pointer identical to " << toString(cloneTy, {true}));
CHECK_EQ(2, dest.typePacks.size()); // one for the function args, and another for its return type
CHECK_EQ(2, dest.types.size()); // One table and one function
}
TEST_CASE_FIXTURE(Fixture, "deepClone_cyclic_table_2")
{
TypeArena src;
TypeId tableTy = src.addType(TableType{});
TableType* tt = getMutable<TableType>(tableTy);
REQUIRE(tt);
TypeId methodTy = src.addType(FunctionType{src.addTypePack({}), src.addTypePack({tableTy})});
tt->props["get"].setType(methodTy);
TypeArena dest;
CloneState cloneState;
TypeId cloneTy = clone(tableTy, dest, cloneState);
TableType* ctt = getMutable<TableType>(cloneTy);
REQUIRE(ctt);
TypeId clonedMethodType = ctt->props["get"].type();
REQUIRE(clonedMethodType);
const FunctionType* cmf = get<FunctionType>(clonedMethodType);
REQUIRE(cmf);
std::optional<TypeId> cloneMethodReturnType = first(cmf->retTypes);
REQUIRE(bool(cloneMethodReturnType));
CHECK(*cloneMethodReturnType == cloneTy);
}
2022-05-14 03:36:37 +08:00
TEST_CASE_FIXTURE(BuiltinsFixture, "builtin_types_point_into_globalTypes_arena")
{
CheckResult result = check(R"(
return {sign=math.sign}
)");
dumpErrors(result);
LUAU_REQUIRE_NO_ERRORS(result);
ModulePtr module = frontend.moduleResolver.getModule("MainModule");
std::optional<TypeId> exports = first(module->returnType);
REQUIRE(bool(exports));
REQUIRE(isInArena(*exports, module->interfaceTypes));
TableType* exportsTable = getMutable<TableType>(*exports);
REQUIRE(exportsTable != nullptr);
TypeId signType = exportsTable->props["sign"].type();
REQUIRE(signType != nullptr);
CHECK(!isInArena(signType, module->interfaceTypes));
CHECK(isInArena(signType, frontend.globals.globalTypes));
}
TEST_CASE_FIXTURE(Fixture, "deepClone_union")
{
TypeArena dest;
CloneState cloneState;
unfreeze(frontend.globals.globalTypes);
TypeId oldUnion = frontend.globals.globalTypes.addType(UnionType{{builtinTypes->numberType, builtinTypes->stringType}});
freeze(frontend.globals.globalTypes);
2022-04-15 07:57:43 +08:00
TypeId newUnion = clone(oldUnion, dest, cloneState);
CHECK_NE(newUnion, oldUnion);
CHECK_EQ("number | string", toString(newUnion));
CHECK_EQ(1, dest.types.size());
}
TEST_CASE_FIXTURE(Fixture, "deepClone_intersection")
{
TypeArena dest;
CloneState cloneState;
unfreeze(frontend.globals.globalTypes);
TypeId oldIntersection = frontend.globals.globalTypes.addType(IntersectionType{{builtinTypes->numberType, builtinTypes->stringType}});
freeze(frontend.globals.globalTypes);
2022-04-15 07:57:43 +08:00
TypeId newIntersection = clone(oldIntersection, dest, cloneState);
CHECK_NE(newIntersection, oldIntersection);
CHECK_EQ("number & string", toString(newIntersection));
CHECK_EQ(1, dest.types.size());
}
TEST_CASE_FIXTURE(Fixture, "clone_class")
{
Type exampleMetaClass{ClassType{"ExampleClassMeta",
{
{"__add", {builtinTypes->anyType}},
},
2022-04-22 05:44:27 +08:00
std::nullopt, std::nullopt, {}, {}, "Test"}};
Type exampleClass{ClassType{"ExampleClass",
{
{"PropOne", {builtinTypes->numberType}},
{"PropTwo", {builtinTypes->stringType}},
},
2022-04-22 05:44:27 +08:00
std::nullopt, &exampleMetaClass, {}, {}, "Test"}};
TypeArena dest;
CloneState cloneState;
2022-04-15 07:57:43 +08:00
TypeId cloned = clone(&exampleClass, dest, cloneState);
const ClassType* ctv = get<ClassType>(cloned);
REQUIRE(ctv != nullptr);
REQUIRE(ctv->metatable);
const ClassType* metatable = get<ClassType>(*ctv->metatable);
REQUIRE(metatable);
CHECK_EQ("ExampleClass", ctv->name);
CHECK_EQ("ExampleClassMeta", metatable->name);
}
2022-04-22 05:44:27 +08:00
TEST_CASE_FIXTURE(Fixture, "clone_free_types")
{
Type freeTy(FreeType{TypeLevel{}});
TypePackVar freeTp(FreeTypePack{TypeLevel{}});
TypeArena dest;
CloneState cloneState;
2022-04-15 07:57:43 +08:00
TypeId clonedTy = clone(&freeTy, dest, cloneState);
CHECK(get<FreeType>(clonedTy));
cloneState = {};
2022-04-15 07:57:43 +08:00
TypePackId clonedTp = clone(&freeTp, dest, cloneState);
2022-04-22 05:44:27 +08:00
CHECK(get<FreeTypePack>(clonedTp));
}
2022-04-22 05:44:27 +08:00
TEST_CASE_FIXTURE(Fixture, "clone_free_tables")
{
Type tableTy{TableType{}};
TableType* ttv = getMutable<TableType>(&tableTy);
ttv->state = TableState::Free;
TypeArena dest;
CloneState cloneState;
2022-04-15 07:57:43 +08:00
TypeId cloned = clone(&tableTy, dest, cloneState);
const TableType* clonedTtv = get<TableType>(cloned);
2022-04-22 05:44:27 +08:00
CHECK_EQ(clonedTtv->state, TableState::Free);
}
2022-05-14 03:36:37 +08:00
TEST_CASE_FIXTURE(BuiltinsFixture, "clone_self_property")
{
fileResolver.source["Module/A"] = R"(
--!nonstrict
local a = {}
2022-03-18 08:46:04 +08:00
function a:foo(x: number)
return -x;
end
return a;
)";
CheckResult result = frontend.check("Module/A");
LUAU_REQUIRE_NO_ERRORS(result);
fileResolver.source["Module/B"] = R"(
--!nonstrict
local a = require(script.Parent.A)
return a.foo(5)
)";
result = frontend.check("Module/B");
2022-03-18 08:46:04 +08:00
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("This function must be called with self. Did you mean to use a colon instead of a dot?", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit")
{
#if defined(_DEBUG) || defined(_NOOPT)
int limit = 250;
#else
int limit = 400;
#endif
ScopedFastInt luauTypeCloneRecursionLimit{"LuauTypeCloneRecursionLimit", limit};
TypeArena src;
TypeId table = src.addType(TableType{});
TypeId nested = table;
for (int i = 0; i < limit + 100; i++)
{
TableType* ttv = getMutable<TableType>(nested);
ttv->props["a"].setType(src.addType(TableType{}));
nested = ttv->props["a"].type();
}
TypeArena dest;
CloneState cloneState;
CHECK_THROWS_AS(clone(table, dest, cloneState), RecursionLimitException);
}
Sync to upstream/release/577 (#934) Lots of things going on this week: * Fix a crash that could occur in the presence of a cyclic union. We shouldn't be creating cyclic unions, but we shouldn't be crashing when they arise either. * Minor cleanup of `luau_precall` * Internal change to make L->top handling slightly more uniform * Optimize SETGLOBAL & GETGLOBAL fallback C functions. * https://github.com/Roblox/luau/pull/929 * The syntax to the `luau-reduce` commandline tool has changed. It now accepts a script, a command to execute, and an error to search for. It no longer automatically passes the script to the command which makes it a lot more flexible. Also be warned that it edits the script it is passed **in place**. Do not point it at something that is not in source control! New solver * Switch to a greedier but more fallible algorithm for simplifying union and intersection types that are created as part of refinement calculation. This has much better and more predictable performance. * Fix a constraint cycle in recursive function calls. * Much improved inference of binary addition. Functions like `function add(x, y) return x + y end` can now be inferred without annotations. We also accurately typecheck calls to functions like this. * Many small bugfixes surrounding things like table indexers * Add support for indexers on class types. This was previously added to the old solver; we now add it to the new one for feature parity. JIT * https://github.com/Roblox/luau/pull/931 * Fuse key.value and key.tt loads for CEHCK_SLOT_MATCH in A64 * Implement remaining aliases of BFM for A64 * Implement new callinfo flag for A64 * Add instruction simplification for int->num->int conversion chains * Don't even load execdata for X64 calls * Treat opcode fallbacks the same as manually written fallbacks --------- Co-authored-by: Arseny Kapoulkine <arseny.kapoulkine@gmail.com> Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
2023-05-20 03:37:30 +08:00
// Unions should never be cyclic, but we should clone them correctly even if
// they are.
TEST_CASE_FIXTURE(Fixture, "clone_cyclic_union")
{
ScopedFastFlag sff{"LuauCloneCyclicUnions", true};
TypeArena src;
TypeId u = src.addType(UnionType{{builtinTypes->numberType, builtinTypes->stringType}});
UnionType* uu = getMutable<UnionType>(u);
REQUIRE(uu);
uu->options.push_back(u);
TypeArena dest;
CloneState cloneState;
TypeId cloned = clone(u, dest, cloneState);
REQUIRE(cloned);
const UnionType* clonedUnion = get<UnionType>(cloned);
REQUIRE(clonedUnion);
REQUIRE(3 == clonedUnion->options.size());
CHECK(builtinTypes->numberType == clonedUnion->options[0]);
CHECK(builtinTypes->stringType == clonedUnion->options[1]);
CHECK(cloned == clonedUnion->options[2]);
}
TEST_CASE_FIXTURE(Fixture, "any_persistance_does_not_leak")
{
Sync to upstream/release/572 (#899) * Fixed exported types not being suggested in autocomplete * `T...` is now convertible to `...any` (Fixes https://github.com/Roblox/luau/issues/767) * Fixed issue with `T?` not being convertible to `T | T` or `T?` (sometimes when internal pointer identity is different) * Fixed potential crash in missing table key error suggestion to use a similar existing key * `lua_topointer` now returns a pointer for strings C++ API Changes: * `prepareModuleScope` callback has moved from TypeChecker to Frontend * For LSPs, AstQuery functions (and `isWithinComment`) can be used without full Frontend data A lot of changes in our two experimental components as well. In our work on the new type-solver, the following issues were fixed: * Fixed table union and intersection indexing * Correct custom type environments are now used * Fixed issue with values of `free & number` type not accepted in numeric operations And these are the changes in native code generation (JIT): * arm64 lowering is almost complete with support for 99% of IR commands and all fastcalls * Fixed x64 assembly encoding for extended byte registers * More external x64 calls are aware of register allocator * `math.min`/`math.max` with more than 2 arguments are now lowered to IR as well * Fixed correctness issues with `math` library calls with multiple results in variadic context and with x64 register conflicts * x64 register allocator learnt to restore values from VM memory instead of always using stack spills * x64 exception unwind information now supports multiple functions and fixes function start offset in Dwarf2 info
2023-04-15 02:06:22 +08:00
ScopedFastFlag flags[] = {
{"LuauOccursIsntAlwaysFailure", true},
};
fileResolver.source["Module/A"] = R"(
export type A = B
type B = A
)";
FrontendOptions opts;
opts.retainFullTypeGraphs = false;
CheckResult result = frontend.check("Module/A", opts);
LUAU_REQUIRE_ERRORS(result);
auto mod = frontend.moduleResolver.getModule("Module/A");
auto it = mod->exportedTypeBindings.find("A");
REQUIRE(it != mod->exportedTypeBindings.end());
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(toString(it->second.type) == "any");
else
CHECK(toString(it->second.type) == "*error-type*");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_reexports")
{
fileResolver.source["Module/A"] = R"(
export type A = {p : number}
return {}
)";
fileResolver.source["Module/B"] = R"(
local a = require(script.Parent.A)
export type B = {q : a.A}
return {}
)";
CheckResult result = frontend.check("Module/B");
LUAU_REQUIRE_NO_ERRORS(result);
ModulePtr modA = frontend.moduleResolver.getModule("Module/A");
ModulePtr modB = frontend.moduleResolver.getModule("Module/B");
REQUIRE(modA);
REQUIRE(modB);
auto modAiter = modA->exportedTypeBindings.find("A");
auto modBiter = modB->exportedTypeBindings.find("B");
REQUIRE(modAiter != modA->exportedTypeBindings.end());
REQUIRE(modBiter != modB->exportedTypeBindings.end());
TypeId typeA = modAiter->second.type;
TypeId typeB = modBiter->second.type;
TableType* tableB = getMutable<TableType>(typeB);
REQUIRE(tableB);
CHECK(typeA == tableB->props["q"].type());
}
TEST_CASE_FIXTURE(BuiltinsFixture, "do_not_clone_types_of_reexported_values")
{
fileResolver.source["Module/A"] = R"(
local exports = {a={p=5}}
return exports
)";
fileResolver.source["Module/B"] = R"(
local a = require(script.Parent.A)
local exports = {b=a.a}
return exports
)";
CheckResult result = frontend.check("Module/B");
LUAU_REQUIRE_NO_ERRORS(result);
ModulePtr modA = frontend.moduleResolver.getModule("Module/A");
ModulePtr modB = frontend.moduleResolver.getModule("Module/B");
REQUIRE(modA);
REQUIRE(modB);
std::optional<TypeId> typeA = first(modA->returnType);
std::optional<TypeId> typeB = first(modB->returnType);
REQUIRE(typeA);
REQUIRE(typeB);
TableType* tableA = getMutable<TableType>(*typeA);
TableType* tableB = getMutable<TableType>(*typeB);
CHECK(tableA->props["a"].type() == tableB->props["b"].type());
}
TEST_SUITE_END();