mirror of
https://github.com/luau-lang/luau.git
synced 2025-01-09 04:49:10 +00:00
8f94786ceb
Some checks failed
benchmark / callgrind (map[branch:main name:luau-lang/benchmark-data], ubuntu-22.04) (push) Has been cancelled
build / macos (push) Has been cancelled
build / macos-arm (push) Has been cancelled
build / ubuntu (push) Has been cancelled
build / windows (Win32) (push) Has been cancelled
build / windows (x64) (push) Has been cancelled
build / coverage (push) Has been cancelled
build / web (push) Has been cancelled
release / macos (push) Has been cancelled
release / ubuntu (push) Has been cancelled
release / windows (push) Has been cancelled
release / web (push) Has been cancelled
This PR refactors the CLI folder to use the same project split between include and src directories that we have for all the other artifacts in luau. It also includes the require-by-string implementation we already have as a feature of `Luau.CLI.lib`. Both of these changes are targeted at making it easier for embedding projects to setup an effective equivalent to the standalone `luau` executable with whatever runtime libraries they need attached and without having to unnecessarily duplicate code from luau itself.
451 lines
12 KiB
C++
451 lines
12 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
#include "lua.h"
|
|
#include "lualib.h"
|
|
|
|
#include "Luau/Repl.h"
|
|
#include "ScopedFlags.h"
|
|
|
|
#include "doctest.h"
|
|
|
|
#include <iostream>
|
|
#include <memory>
|
|
#include <set>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
LUAU_FASTFLAG(LuauMathMap)
|
|
|
|
struct Completion
|
|
{
|
|
std::string completion;
|
|
std::string display;
|
|
|
|
bool operator<(Completion const& other) const
|
|
{
|
|
return std::tie(completion, display) < std::tie(other.completion, other.display);
|
|
}
|
|
};
|
|
|
|
using CompletionSet = std::set<Completion>;
|
|
|
|
class ReplFixture
|
|
{
|
|
public:
|
|
ReplFixture()
|
|
: luaState(luaL_newstate(), lua_close)
|
|
{
|
|
L = luaState.get();
|
|
setupState(L);
|
|
luaL_sandboxthread(L);
|
|
|
|
std::string result = runCode(L, prettyPrintSource);
|
|
}
|
|
|
|
// Returns all of the output captured from the pretty printer
|
|
std::string getCapturedOutput()
|
|
{
|
|
lua_getglobal(L, "capturedoutput");
|
|
const char* str = lua_tolstring(L, -1, nullptr);
|
|
std::string result(str);
|
|
lua_pop(L, 1);
|
|
return result;
|
|
}
|
|
|
|
CompletionSet getCompletionSet(const char* inputPrefix)
|
|
{
|
|
CompletionSet result;
|
|
int top = lua_gettop(L);
|
|
getCompletions(
|
|
L,
|
|
inputPrefix,
|
|
[&result](const std::string& completion, const std::string& display)
|
|
{
|
|
result.insert(Completion{completion, display});
|
|
}
|
|
);
|
|
// Ensure that generating completions doesn't change the position of luau's stack top.
|
|
CHECK(top == lua_gettop(L));
|
|
|
|
return result;
|
|
}
|
|
|
|
bool checkCompletion(const CompletionSet& completions, const std::string& prefix, const std::string& expected)
|
|
{
|
|
std::string expectedDisplay(expected.substr(0, expected.find_first_of('(')));
|
|
Completion expectedCompletion{prefix + expected, expectedDisplay};
|
|
return completions.count(expectedCompletion) == 1;
|
|
}
|
|
|
|
lua_State* L;
|
|
|
|
private:
|
|
std::unique_ptr<lua_State, void (*)(lua_State*)> luaState;
|
|
|
|
// This is a simplistic and incomplete pretty printer.
|
|
// It is included here to test that the pretty printer hook is being called.
|
|
// More elaborate tests to ensure correct output can be added if we introduce
|
|
// a more feature rich pretty printer.
|
|
std::string prettyPrintSource = R"(
|
|
-- Accumulate pretty printer output in `capturedoutput`
|
|
capturedoutput = ""
|
|
|
|
function arraytostring(arr)
|
|
local strings = {}
|
|
table.foreachi(arr, function(k,v) table.insert(strings, pptostring(v)) end )
|
|
return "{" .. table.concat(strings, ", ") .. "}"
|
|
end
|
|
|
|
function pptostring(x)
|
|
if type(x) == "table" then
|
|
-- Just assume array-like tables for now.
|
|
return arraytostring(x)
|
|
elseif type(x) == "string" then
|
|
return '"' .. x .. '"'
|
|
else
|
|
return tostring(x)
|
|
end
|
|
end
|
|
|
|
-- Note: Instead of calling print, the pretty printer just stores the output
|
|
-- in `capturedoutput` so we can check for the correct results.
|
|
function _PRETTYPRINT(...)
|
|
local args = table.pack(...)
|
|
local strings = {}
|
|
for i=1, args.n do
|
|
local item = args[i]
|
|
local str = pptostring(item, customoptions)
|
|
if i == 1 then
|
|
capturedoutput = capturedoutput .. str
|
|
else
|
|
capturedoutput = capturedoutput .. "\t" .. str
|
|
end
|
|
end
|
|
end
|
|
)";
|
|
};
|
|
|
|
TEST_SUITE_BEGIN("ReplPrettyPrint");
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "AdditionStatement")
|
|
{
|
|
runCode(L, "return 30 + 12");
|
|
CHECK(getCapturedOutput() == "42");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "TableLiteral")
|
|
{
|
|
runCode(L, "return {1, 2, 3, 4}");
|
|
CHECK(getCapturedOutput() == "{1, 2, 3, 4}");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "StringLiteral")
|
|
{
|
|
runCode(L, "return 'str'");
|
|
CHECK(getCapturedOutput() == "\"str\"");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "TableWithStringLiterals")
|
|
{
|
|
runCode(L, "return {1, 'two', 3, 'four'}");
|
|
CHECK(getCapturedOutput() == "{1, \"two\", 3, \"four\"}");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "MultipleArguments")
|
|
{
|
|
runCode(L, "return 3, 'three'");
|
|
CHECK(getCapturedOutput() == "3\t\"three\"");
|
|
}
|
|
|
|
TEST_SUITE_END();
|
|
|
|
TEST_SUITE_BEGIN("ReplCodeCompletion");
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "CompleteGlobalVariables")
|
|
{
|
|
runCode(L, R"(
|
|
myvariable1 = 5
|
|
myvariable2 = 5
|
|
)");
|
|
{
|
|
// Try to complete globals that are added by the user's script
|
|
CompletionSet completions = getCompletionSet("myvar");
|
|
|
|
std::string prefix = "";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "myvariable1"));
|
|
CHECK(checkCompletion(completions, prefix, "myvariable2"));
|
|
}
|
|
if (FFlag::LuauMathMap)
|
|
{
|
|
// Try completing some builtin functions
|
|
CompletionSet completions = getCompletionSet("math.m");
|
|
|
|
std::string prefix = "math.";
|
|
CHECK(completions.size() == 4);
|
|
CHECK(checkCompletion(completions, prefix, "max("));
|
|
CHECK(checkCompletion(completions, prefix, "min("));
|
|
CHECK(checkCompletion(completions, prefix, "modf("));
|
|
CHECK(checkCompletion(completions, prefix, "map("));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "CompleteTableKeys")
|
|
{
|
|
runCode(L, R"(
|
|
t = { color = "red", size = 1, shape = "circle" }
|
|
)");
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.");
|
|
|
|
std::string prefix = "t.";
|
|
CHECK(completions.size() == 3);
|
|
CHECK(checkCompletion(completions, prefix, "color"));
|
|
CHECK(checkCompletion(completions, prefix, "size"));
|
|
CHECK(checkCompletion(completions, prefix, "shape"));
|
|
}
|
|
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.s");
|
|
|
|
std::string prefix = "t.";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "size"));
|
|
CHECK(checkCompletion(completions, prefix, "shape"));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "StringMethods")
|
|
{
|
|
runCode(L, R"(
|
|
s = ""
|
|
)");
|
|
{
|
|
CompletionSet completions = getCompletionSet("s:l");
|
|
|
|
std::string prefix = "s:";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "len("));
|
|
CHECK(checkCompletion(completions, prefix, "lower("));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "TableWithMetatableIndexTable")
|
|
{
|
|
runCode(L, R"(
|
|
-- Create 't' which is a table with a metatable with an __index table
|
|
mt = {}
|
|
mt.__index = mt
|
|
|
|
t = {}
|
|
setmetatable(t, mt)
|
|
|
|
mt.mtkey1 = {x="x value", y="y value", 1, 2}
|
|
mt.mtkey2 = 2
|
|
|
|
t.tkey1 = {data1 = 2, data2 = "str", 3, 4}
|
|
t.tkey2 = 4
|
|
)");
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.t");
|
|
|
|
std::string prefix = "t.";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "tkey1"));
|
|
CHECK(checkCompletion(completions, prefix, "tkey2"));
|
|
}
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.tkey1.data2:re");
|
|
|
|
std::string prefix = "t.tkey1.data2:";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "rep("));
|
|
CHECK(checkCompletion(completions, prefix, "reverse("));
|
|
}
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.mtk");
|
|
|
|
std::string prefix = "t.";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "mtkey1"));
|
|
CHECK(checkCompletion(completions, prefix, "mtkey2"));
|
|
}
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.mtkey1.");
|
|
|
|
std::string prefix = "t.mtkey1.";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "x"));
|
|
CHECK(checkCompletion(completions, prefix, "y"));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "TableWithMetatableIndexFunction")
|
|
{
|
|
runCode(L, R"(
|
|
-- Create 't' which is a table with a metatable with an __index function
|
|
mt = {}
|
|
mt.__index = function(table, key)
|
|
print("mt.__index called")
|
|
if key == "foo" then
|
|
return "FOO"
|
|
elseif key == "bar" then
|
|
return "BAR"
|
|
else
|
|
return nil
|
|
end
|
|
end
|
|
|
|
t = {}
|
|
setmetatable(t, mt)
|
|
t.tkey = 0
|
|
)");
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.t");
|
|
|
|
std::string prefix = "t.";
|
|
CHECK(completions.size() == 1);
|
|
CHECK(checkCompletion(completions, prefix, "tkey"));
|
|
}
|
|
{
|
|
// t.foo is a valid key, but should not be completed because it requires calling an __index function
|
|
CompletionSet completions = getCompletionSet("t.foo");
|
|
|
|
CHECK(completions.size() == 0);
|
|
}
|
|
{
|
|
// t.foo is a valid key, but should not be found because it requires calling an __index function
|
|
CompletionSet completions = getCompletionSet("t.foo:");
|
|
|
|
CHECK(completions.size() == 0);
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "TableWithMultipleMetatableIndexTables")
|
|
{
|
|
runCode(L, R"(
|
|
-- Create a table with a chain of metatables
|
|
mt2 = {}
|
|
mt2.__index = mt2
|
|
|
|
mt = {}
|
|
mt.__index = mt
|
|
setmetatable(mt, mt2)
|
|
|
|
t = {}
|
|
setmetatable(t, mt)
|
|
|
|
mt2.mt2key = {x=1, y=2}
|
|
mt.mtkey = 2
|
|
t.tkey = 3
|
|
)");
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.");
|
|
|
|
std::string prefix = "t.";
|
|
CHECK(completions.size() == 4);
|
|
CHECK(checkCompletion(completions, prefix, "__index"));
|
|
CHECK(checkCompletion(completions, prefix, "tkey"));
|
|
CHECK(checkCompletion(completions, prefix, "mtkey"));
|
|
CHECK(checkCompletion(completions, prefix, "mt2key"));
|
|
}
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.__index.");
|
|
|
|
std::string prefix = "t.__index.";
|
|
CHECK(completions.size() == 3);
|
|
CHECK(checkCompletion(completions, prefix, "__index"));
|
|
CHECK(checkCompletion(completions, prefix, "mtkey"));
|
|
CHECK(checkCompletion(completions, prefix, "mt2key"));
|
|
}
|
|
{
|
|
CompletionSet completions = getCompletionSet("t.mt2key.");
|
|
|
|
std::string prefix = "t.mt2key.";
|
|
CHECK(completions.size() == 2);
|
|
CHECK(checkCompletion(completions, prefix, "x"));
|
|
CHECK(checkCompletion(completions, prefix, "y"));
|
|
}
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "TableWithDeepMetatableIndexTables")
|
|
{
|
|
runCode(L, R"(
|
|
-- Creates a table with a chain of metatables of length `count`
|
|
function makeChainedTable(count)
|
|
local result = {}
|
|
result.__index = result
|
|
result[string.format("entry%d", count)] = { count = count }
|
|
if count == 0 then
|
|
return result
|
|
else
|
|
return setmetatable(result, makeChainedTable(count - 1))
|
|
end
|
|
end
|
|
|
|
t30 = makeChainedTable(30)
|
|
t60 = makeChainedTable(60)
|
|
)");
|
|
{
|
|
// Check if entry0 exists
|
|
CompletionSet completions = getCompletionSet("t30.entry0");
|
|
|
|
std::string prefix = "t30.";
|
|
CHECK(checkCompletion(completions, prefix, "entry0"));
|
|
}
|
|
{
|
|
// Check if entry0.count exists
|
|
CompletionSet completions = getCompletionSet("t30.entry0.co");
|
|
|
|
std::string prefix = "t30.entry0.";
|
|
CHECK(checkCompletion(completions, prefix, "count"));
|
|
}
|
|
{
|
|
// Check if entry0 exists. With the max traversal limit of 50 in the repl, this should fail.
|
|
CompletionSet completions = getCompletionSet("t60.entry0");
|
|
|
|
CHECK(completions.size() == 0);
|
|
}
|
|
{
|
|
// Check if entry0.count exists. With the max traversal limit of 50 in the repl, this should fail.
|
|
CompletionSet completions = getCompletionSet("t60.entry0.co");
|
|
|
|
CHECK(completions.size() == 0);
|
|
}
|
|
}
|
|
|
|
TEST_SUITE_END();
|
|
|
|
TEST_SUITE_BEGIN("RegressionTests");
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "InfiniteRecursion")
|
|
{
|
|
// If the infinite recrusion is not caught, test will fail
|
|
runCode(L, R"(
|
|
local NewProxyOne = newproxy(true)
|
|
local MetaTableOne = getmetatable(NewProxyOne)
|
|
MetaTableOne.__index = function()
|
|
return NewProxyOne.Game
|
|
end
|
|
print(NewProxyOne.HelloICauseACrash)
|
|
)");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "InteractiveStackReserve1")
|
|
{
|
|
// Reset stack reservation
|
|
lua_resume(L, nullptr, 0);
|
|
|
|
runCode(L, R"(
|
|
local t = {}
|
|
)");
|
|
}
|
|
|
|
TEST_CASE_FIXTURE(ReplFixture, "InteractiveStackReserve2")
|
|
{
|
|
// Reset stack reservation
|
|
lua_resume(L, nullptr, 0);
|
|
|
|
getCompletionSet("a");
|
|
}
|
|
|
|
TEST_SUITE_END();
|