mirror of
https://github.com/luau-lang/luau.git
synced 2024-12-12 21:10:37 +00:00
97965c7c0a
* `ClassType` can now have an indexer defined on it. This allows custom types to be used in `t[x]` expressions. * Fixed search for closest executable breakpoint line. Previously, breakpoints might have been skipped in `else` blocks at the end of a function * Fixed how unification is performed for two optional types `a? <: b?`, previously it might have unified either 'a' or 'b' with 'nil'. Note that this fix is not enabled by default yet (see the list in `ExperimentalFlags.h`) In the new type solver, a concept of 'Type Families' has been introduced. Type families can be thought of as type aliases with custom type inference/reduction logic included with them. For example, we can have an `Add<T, U>` type family that will resolve the type that is the result of adding two values together. This will help type inference to figure out what 'T' and 'U' might be when explicit type annotations are not provided. In this update we don't define any type families, but they will be added in the near future. It is also possible for Luau embedders to define their own type families in the global/environment scope. Other changes include: * Fixed scope used to find out which generic types should be included in the function generic type list * Fixed a crash after cyclic bound types were created during unification And in native code generation (jit): * Use of arm64 target on M1 now requires macOS 13 * Entry into native code has been optimized. This is especially important for coroutine call/pcall performance as they involve going through a C call frame * LOP_LOADK(X) translation into IR has been improved to enable type tag/constant propagation * arm64 can use integer immediate values to synthesize floating-point values * x64 assembler removes duplicate 64bit numbers from the data section to save space * Linux `perf` can now be used to profile native Luau code (when running with --codegen-perf CLI argument)
1109 lines
31 KiB
C++
1109 lines
31 KiB
C++
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
|
|
#include "Repl.h"
|
|
|
|
#include "lua.h"
|
|
#include "lualib.h"
|
|
|
|
#include "Luau/CodeGen.h"
|
|
#include "Luau/Compiler.h"
|
|
#include "Luau/BytecodeBuilder.h"
|
|
#include "Luau/Parser.h"
|
|
#include "Luau/TimeTrace.h"
|
|
|
|
#include "Coverage.h"
|
|
#include "FileUtils.h"
|
|
#include "Flags.h"
|
|
#include "Profiler.h"
|
|
|
|
#include "isocline.h"
|
|
|
|
#include <memory>
|
|
|
|
#ifdef _WIN32
|
|
#include <io.h>
|
|
#include <fcntl.h>
|
|
|
|
#define WIN32_LEAN_AND_MEAN
|
|
#include <windows.h>
|
|
#endif
|
|
|
|
#ifdef __linux__
|
|
#include <unistd.h>
|
|
#endif
|
|
|
|
#ifdef CALLGRIND
|
|
#include <valgrind/callgrind.h>
|
|
#endif
|
|
|
|
#include <locale.h>
|
|
#include <signal.h>
|
|
|
|
LUAU_FASTFLAG(DebugLuauTimeTracing)
|
|
|
|
enum class CliMode
|
|
{
|
|
Unknown,
|
|
Repl,
|
|
Compile,
|
|
RunSourceFiles
|
|
};
|
|
|
|
enum class CompileFormat
|
|
{
|
|
Text,
|
|
Binary,
|
|
Remarks,
|
|
Codegen, // Prints annotated native code including IR and assembly
|
|
CodegenAsm, // Prints annotated native code assembly
|
|
CodegenIr, // Prints annotated native code IR
|
|
CodegenVerbose, // Prints annotated native code including IR, assembly and outlined code
|
|
CodegenNull,
|
|
Null
|
|
};
|
|
|
|
constexpr int MaxTraversalLimit = 50;
|
|
|
|
static bool codegen = false;
|
|
|
|
// Ctrl-C handling
|
|
static void sigintCallback(lua_State* L, int gc)
|
|
{
|
|
if (gc >= 0)
|
|
return;
|
|
|
|
lua_callbacks(L)->interrupt = NULL;
|
|
|
|
lua_rawcheckstack(L, 1); // reserve space for error string
|
|
luaL_error(L, "Execution interrupted");
|
|
}
|
|
|
|
static lua_State* replState = NULL;
|
|
|
|
#ifdef _WIN32
|
|
BOOL WINAPI sigintHandler(DWORD signal)
|
|
{
|
|
if (signal == CTRL_C_EVENT && replState)
|
|
lua_callbacks(replState)->interrupt = &sigintCallback;
|
|
return TRUE;
|
|
}
|
|
#else
|
|
static void sigintHandler(int signum)
|
|
{
|
|
if (signum == SIGINT && replState)
|
|
lua_callbacks(replState)->interrupt = &sigintCallback;
|
|
}
|
|
#endif
|
|
|
|
struct GlobalOptions
|
|
{
|
|
int optimizationLevel = 1;
|
|
int debugLevel = 1;
|
|
} globalOptions;
|
|
|
|
static Luau::CompileOptions copts()
|
|
{
|
|
Luau::CompileOptions result = {};
|
|
result.optimizationLevel = globalOptions.optimizationLevel;
|
|
result.debugLevel = globalOptions.debugLevel;
|
|
result.coverageLevel = coverageActive() ? 2 : 0;
|
|
|
|
return result;
|
|
}
|
|
|
|
static int lua_loadstring(lua_State* L)
|
|
{
|
|
size_t l = 0;
|
|
const char* s = luaL_checklstring(L, 1, &l);
|
|
const char* chunkname = luaL_optstring(L, 2, s);
|
|
|
|
lua_setsafeenv(L, LUA_ENVIRONINDEX, false);
|
|
|
|
std::string bytecode = Luau::compile(std::string(s, l), copts());
|
|
if (luau_load(L, chunkname, bytecode.data(), bytecode.size(), 0) == 0)
|
|
return 1;
|
|
|
|
lua_pushnil(L);
|
|
lua_insert(L, -2); // put before error message
|
|
return 2; // return nil plus error message
|
|
}
|
|
|
|
static int finishrequire(lua_State* L)
|
|
{
|
|
if (lua_isstring(L, -1))
|
|
lua_error(L);
|
|
|
|
return 1;
|
|
}
|
|
|
|
static int lua_require(lua_State* L)
|
|
{
|
|
std::string name = luaL_checkstring(L, 1);
|
|
std::string chunkname = "=" + name;
|
|
|
|
luaL_findtable(L, LUA_REGISTRYINDEX, "_MODULES", 1);
|
|
|
|
// return the module from the cache
|
|
lua_getfield(L, -1, name.c_str());
|
|
if (!lua_isnil(L, -1))
|
|
{
|
|
// L stack: _MODULES result
|
|
return finishrequire(L);
|
|
}
|
|
|
|
lua_pop(L, 1);
|
|
|
|
std::optional<std::string> source = readFile(name + ".luau");
|
|
if (!source)
|
|
{
|
|
source = readFile(name + ".lua"); // try .lua if .luau doesn't exist
|
|
if (!source)
|
|
luaL_argerrorL(L, 1, ("error loading " + name).c_str()); // if neither .luau nor .lua exist, we have an error
|
|
}
|
|
|
|
// module needs to run in a new thread, isolated from the rest
|
|
// note: we create ML on main thread so that it doesn't inherit environment of L
|
|
lua_State* GL = lua_mainthread(L);
|
|
lua_State* ML = lua_newthread(GL);
|
|
lua_xmove(GL, L, 1);
|
|
|
|
// new thread needs to have the globals sandboxed
|
|
luaL_sandboxthread(ML);
|
|
|
|
// now we can compile & run module on the new thread
|
|
std::string bytecode = Luau::compile(*source, copts());
|
|
if (luau_load(ML, chunkname.c_str(), bytecode.data(), bytecode.size(), 0) == 0)
|
|
{
|
|
if (codegen)
|
|
Luau::CodeGen::compile(ML, -1);
|
|
|
|
if (coverageActive())
|
|
coverageTrack(ML, -1);
|
|
|
|
int status = lua_resume(ML, L, 0);
|
|
|
|
if (status == 0)
|
|
{
|
|
if (lua_gettop(ML) == 0)
|
|
lua_pushstring(ML, "module must return a value");
|
|
else if (!lua_istable(ML, -1) && !lua_isfunction(ML, -1))
|
|
lua_pushstring(ML, "module must return a table or function");
|
|
}
|
|
else if (status == LUA_YIELD)
|
|
{
|
|
lua_pushstring(ML, "module can not yield");
|
|
}
|
|
else if (!lua_isstring(ML, -1))
|
|
{
|
|
lua_pushstring(ML, "unknown error while running module");
|
|
}
|
|
}
|
|
|
|
// there's now a return value on top of ML; L stack: _MODULES ML
|
|
lua_xmove(ML, L, 1);
|
|
lua_pushvalue(L, -1);
|
|
lua_setfield(L, -4, name.c_str());
|
|
|
|
// L stack: _MODULES ML result
|
|
return finishrequire(L);
|
|
}
|
|
|
|
static int lua_collectgarbage(lua_State* L)
|
|
{
|
|
const char* option = luaL_optstring(L, 1, "collect");
|
|
|
|
if (strcmp(option, "collect") == 0)
|
|
{
|
|
lua_gc(L, LUA_GCCOLLECT, 0);
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(option, "count") == 0)
|
|
{
|
|
int c = lua_gc(L, LUA_GCCOUNT, 0);
|
|
lua_pushnumber(L, c);
|
|
return 1;
|
|
}
|
|
|
|
luaL_error(L, "collectgarbage must be called with 'count' or 'collect'");
|
|
}
|
|
|
|
#ifdef CALLGRIND
|
|
static int lua_callgrind(lua_State* L)
|
|
{
|
|
const char* option = luaL_checkstring(L, 1);
|
|
|
|
if (strcmp(option, "running") == 0)
|
|
{
|
|
int r = RUNNING_ON_VALGRIND;
|
|
lua_pushboolean(L, r);
|
|
return 1;
|
|
}
|
|
|
|
if (strcmp(option, "zero") == 0)
|
|
{
|
|
CALLGRIND_ZERO_STATS;
|
|
return 0;
|
|
}
|
|
|
|
if (strcmp(option, "dump") == 0)
|
|
{
|
|
const char* name = luaL_checkstring(L, 2);
|
|
|
|
CALLGRIND_DUMP_STATS_AT(name);
|
|
return 0;
|
|
}
|
|
|
|
luaL_error(L, "callgrind must be called with one of 'running', 'zero', 'dump'");
|
|
}
|
|
#endif
|
|
|
|
void setupState(lua_State* L)
|
|
{
|
|
if (codegen)
|
|
Luau::CodeGen::create(L);
|
|
|
|
luaL_openlibs(L);
|
|
|
|
static const luaL_Reg funcs[] = {
|
|
{"loadstring", lua_loadstring},
|
|
{"require", lua_require},
|
|
{"collectgarbage", lua_collectgarbage},
|
|
#ifdef CALLGRIND
|
|
{"callgrind", lua_callgrind},
|
|
#endif
|
|
{NULL, NULL},
|
|
};
|
|
|
|
lua_pushvalue(L, LUA_GLOBALSINDEX);
|
|
luaL_register(L, NULL, funcs);
|
|
lua_pop(L, 1);
|
|
|
|
luaL_sandbox(L);
|
|
}
|
|
|
|
std::string runCode(lua_State* L, const std::string& source)
|
|
{
|
|
std::string bytecode = Luau::compile(source, copts());
|
|
|
|
if (luau_load(L, "=stdin", bytecode.data(), bytecode.size(), 0) != 0)
|
|
{
|
|
size_t len;
|
|
const char* msg = lua_tolstring(L, -1, &len);
|
|
|
|
std::string error(msg, len);
|
|
lua_pop(L, 1);
|
|
|
|
return error;
|
|
}
|
|
|
|
if (codegen)
|
|
Luau::CodeGen::compile(L, -1);
|
|
|
|
lua_State* T = lua_newthread(L);
|
|
|
|
lua_pushvalue(L, -2);
|
|
lua_remove(L, -3);
|
|
lua_xmove(L, T, 1);
|
|
|
|
int status = lua_resume(T, NULL, 0);
|
|
|
|
if (status == 0)
|
|
{
|
|
int n = lua_gettop(T);
|
|
|
|
if (n)
|
|
{
|
|
luaL_checkstack(T, LUA_MINSTACK, "too many results to print");
|
|
lua_getglobal(T, "_PRETTYPRINT");
|
|
// If _PRETTYPRINT is nil, then use the standard print function instead
|
|
if (lua_isnil(T, -1))
|
|
{
|
|
lua_pop(T, 1);
|
|
lua_getglobal(T, "print");
|
|
}
|
|
lua_insert(T, 1);
|
|
lua_pcall(T, n, 0, 0);
|
|
}
|
|
|
|
lua_pop(L, 1);
|
|
return std::string();
|
|
}
|
|
else
|
|
{
|
|
std::string error;
|
|
|
|
if (status == LUA_YIELD)
|
|
{
|
|
error = "thread yielded unexpectedly";
|
|
}
|
|
else if (const char* str = lua_tostring(T, -1))
|
|
{
|
|
error = str;
|
|
}
|
|
|
|
error += "\nstack backtrace:\n";
|
|
error += lua_debugtrace(T);
|
|
|
|
lua_pop(L, 1);
|
|
return error;
|
|
}
|
|
}
|
|
|
|
// Replaces the top of the lua stack with the metatable __index for the value
|
|
// if it exists. Returns true iff __index exists.
|
|
static bool tryReplaceTopWithIndex(lua_State* L)
|
|
{
|
|
if (luaL_getmetafield(L, -1, "__index"))
|
|
{
|
|
// Remove the table leaving __index on the top of stack
|
|
lua_remove(L, -2);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
|
|
// This function is similar to lua_gettable, but it avoids calling any
|
|
// lua callback functions (e.g. __index) which might modify the Lua VM state.
|
|
static void safeGetTable(lua_State* L, int tableIndex)
|
|
{
|
|
lua_pushvalue(L, tableIndex); // Duplicate the table
|
|
|
|
// The loop invariant is that the table to search is at -1
|
|
// and the key is at -2.
|
|
for (int loopCount = 0;; loopCount++)
|
|
{
|
|
lua_pushvalue(L, -2); // Duplicate the key
|
|
lua_rawget(L, -2); // Try to find the key
|
|
if (!lua_isnil(L, -1) || loopCount >= MaxTraversalLimit)
|
|
{
|
|
// Either the key has been found, and/or we have reached the max traversal limit
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
lua_pop(L, 1); // Pop the nil result
|
|
if (!luaL_getmetafield(L, -1, "__index"))
|
|
{
|
|
lua_pushnil(L);
|
|
break;
|
|
}
|
|
else if (lua_istable(L, -1))
|
|
{
|
|
// Replace the current table being searched with __index table
|
|
lua_replace(L, -2);
|
|
}
|
|
else
|
|
{
|
|
lua_pop(L, 1); // Pop the value
|
|
lua_pushnil(L);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
lua_remove(L, -2); // Remove the table
|
|
lua_remove(L, -2); // Remove the original key
|
|
}
|
|
|
|
// completePartialMatches finds keys that match the specified 'prefix'
|
|
// Note: the table/object to be searched must be on the top of the Lua stack
|
|
static void completePartialMatches(lua_State* L, bool completeOnlyFunctions, const std::string& editBuffer, std::string_view prefix,
|
|
const AddCompletionCallback& addCompletionCallback)
|
|
{
|
|
for (int i = 0; i < MaxTraversalLimit && lua_istable(L, -1); i++)
|
|
{
|
|
// table, key
|
|
lua_pushnil(L);
|
|
|
|
// Loop over all the keys in the current table
|
|
while (lua_next(L, -2) != 0)
|
|
{
|
|
if (lua_type(L, -2) == LUA_TSTRING)
|
|
{
|
|
// table, key, value
|
|
std::string_view key = lua_tostring(L, -2);
|
|
int valueType = lua_type(L, -1);
|
|
|
|
// If the last separator was a ':' (i.e. a method call) then only functions should be completed.
|
|
bool requiredValueType = (!completeOnlyFunctions || valueType == LUA_TFUNCTION);
|
|
|
|
if (!key.empty() && requiredValueType && Luau::startsWith(key, prefix))
|
|
{
|
|
std::string completedComponent(key.substr(prefix.size()));
|
|
std::string completion(editBuffer + completedComponent);
|
|
if (valueType == LUA_TFUNCTION)
|
|
{
|
|
// Add an opening paren for function calls by default.
|
|
completion += "(";
|
|
}
|
|
addCompletionCallback(completion, std::string(key));
|
|
}
|
|
}
|
|
lua_pop(L, 1);
|
|
}
|
|
|
|
// Replace the current table being searched with an __index table if one exists
|
|
if (!tryReplaceTopWithIndex(L))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
static void completeIndexer(lua_State* L, const std::string& editBuffer, const AddCompletionCallback& addCompletionCallback)
|
|
{
|
|
std::string_view lookup = editBuffer;
|
|
bool completeOnlyFunctions = false;
|
|
|
|
// Push the global variable table to begin the search
|
|
lua_pushvalue(L, LUA_GLOBALSINDEX);
|
|
|
|
for (;;)
|
|
{
|
|
size_t sep = lookup.find_first_of(".:");
|
|
std::string_view prefix = lookup.substr(0, sep);
|
|
|
|
if (sep == std::string_view::npos)
|
|
{
|
|
completePartialMatches(L, completeOnlyFunctions, editBuffer, prefix, addCompletionCallback);
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
// find the key in the table
|
|
lua_pushlstring(L, prefix.data(), prefix.size());
|
|
safeGetTable(L, -2);
|
|
lua_remove(L, -2);
|
|
|
|
if (lua_istable(L, -1) || tryReplaceTopWithIndex(L))
|
|
{
|
|
completeOnlyFunctions = lookup[sep] == ':';
|
|
lookup.remove_prefix(sep + 1);
|
|
}
|
|
else
|
|
{
|
|
// Unable to search for keys, so stop searching
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
lua_pop(L, 1);
|
|
}
|
|
|
|
void getCompletions(lua_State* L, const std::string& editBuffer, const AddCompletionCallback& addCompletionCallback)
|
|
{
|
|
completeIndexer(L, editBuffer, addCompletionCallback);
|
|
}
|
|
|
|
static void icGetCompletions(ic_completion_env_t* cenv, const char* editBuffer)
|
|
{
|
|
auto* L = reinterpret_cast<lua_State*>(ic_completion_arg(cenv));
|
|
|
|
getCompletions(L, std::string(editBuffer), [cenv](const std::string& completion, const std::string& display) {
|
|
ic_add_completion_ex(cenv, completion.data(), display.data(), nullptr);
|
|
});
|
|
}
|
|
|
|
static bool isMethodOrFunctionChar(const char* s, long len)
|
|
{
|
|
char c = *s;
|
|
return len == 1 && (isalnum(c) || c == '.' || c == ':' || c == '_');
|
|
}
|
|
|
|
static void completeRepl(ic_completion_env_t* cenv, const char* editBuffer)
|
|
{
|
|
ic_complete_word(cenv, editBuffer, icGetCompletions, isMethodOrFunctionChar);
|
|
}
|
|
|
|
static void loadHistory(const char* name)
|
|
{
|
|
std::string path;
|
|
|
|
if (const char* home = getenv("HOME"))
|
|
{
|
|
path = joinPaths(home, name);
|
|
}
|
|
else if (const char* userProfile = getenv("USERPROFILE"))
|
|
{
|
|
path = joinPaths(userProfile, name);
|
|
}
|
|
|
|
if (!path.empty())
|
|
ic_set_history(path.c_str(), -1 /* default entries (= 200) */);
|
|
}
|
|
|
|
static void runReplImpl(lua_State* L)
|
|
{
|
|
ic_set_default_completer(completeRepl, L);
|
|
|
|
// Reset the locale to C
|
|
setlocale(LC_ALL, "C");
|
|
|
|
// Make brace matching easier to see
|
|
ic_style_def("ic-bracematch", "teal");
|
|
|
|
// Prevent auto insertion of braces
|
|
ic_enable_brace_insertion(false);
|
|
|
|
// Loads history from the given file; isocline automatically saves the history on process exit
|
|
loadHistory(".luau_history");
|
|
|
|
std::string buffer;
|
|
|
|
for (;;)
|
|
{
|
|
const char* prompt = buffer.empty() ? "" : ">";
|
|
std::unique_ptr<char, void (*)(void*)> line(ic_readline(prompt), free);
|
|
if (!line)
|
|
break;
|
|
|
|
if (buffer.empty() && runCode(L, std::string("return ") + line.get()) == std::string())
|
|
{
|
|
ic_history_add(line.get());
|
|
continue;
|
|
}
|
|
|
|
if (!buffer.empty())
|
|
buffer += "\n";
|
|
buffer += line.get();
|
|
|
|
std::string error = runCode(L, buffer);
|
|
|
|
if (error.length() >= 5 && error.compare(error.length() - 5, 5, "<eof>") == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (error.length())
|
|
{
|
|
fprintf(stdout, "%s\n", error.c_str());
|
|
}
|
|
|
|
ic_history_add(buffer.c_str());
|
|
buffer.clear();
|
|
}
|
|
}
|
|
|
|
static void runRepl()
|
|
{
|
|
std::unique_ptr<lua_State, void (*)(lua_State*)> globalState(luaL_newstate(), lua_close);
|
|
lua_State* L = globalState.get();
|
|
|
|
setupState(L);
|
|
|
|
// setup Ctrl+C handling
|
|
replState = L;
|
|
#ifdef _WIN32
|
|
SetConsoleCtrlHandler(sigintHandler, TRUE);
|
|
#else
|
|
signal(SIGINT, sigintHandler);
|
|
#endif
|
|
|
|
luaL_sandboxthread(L);
|
|
runReplImpl(L);
|
|
}
|
|
|
|
// `repl` is used it indicate if a repl should be started after executing the file.
|
|
static bool runFile(const char* name, lua_State* GL, bool repl)
|
|
{
|
|
std::optional<std::string> source = readFile(name);
|
|
if (!source)
|
|
{
|
|
fprintf(stderr, "Error opening %s\n", name);
|
|
return false;
|
|
}
|
|
|
|
// module needs to run in a new thread, isolated from the rest
|
|
lua_State* L = lua_newthread(GL);
|
|
|
|
// new thread needs to have the globals sandboxed
|
|
luaL_sandboxthread(L);
|
|
|
|
std::string chunkname = "=" + std::string(name);
|
|
|
|
std::string bytecode = Luau::compile(*source, copts());
|
|
int status = 0;
|
|
|
|
if (luau_load(L, chunkname.c_str(), bytecode.data(), bytecode.size(), 0) == 0)
|
|
{
|
|
if (codegen)
|
|
Luau::CodeGen::compile(L, -1);
|
|
|
|
if (coverageActive())
|
|
coverageTrack(L, -1);
|
|
|
|
status = lua_resume(L, NULL, 0);
|
|
}
|
|
else
|
|
{
|
|
status = LUA_ERRSYNTAX;
|
|
}
|
|
|
|
if (status != 0)
|
|
{
|
|
std::string error;
|
|
|
|
if (status == LUA_YIELD)
|
|
{
|
|
error = "thread yielded unexpectedly";
|
|
}
|
|
else if (const char* str = lua_tostring(L, -1))
|
|
{
|
|
error = str;
|
|
}
|
|
|
|
error += "\nstacktrace:\n";
|
|
error += lua_debugtrace(L);
|
|
|
|
fprintf(stderr, "%s", error.c_str());
|
|
}
|
|
|
|
if (repl)
|
|
{
|
|
runReplImpl(L);
|
|
}
|
|
lua_pop(GL, 1);
|
|
return status == 0;
|
|
}
|
|
|
|
static void report(const char* name, const Luau::Location& location, const char* type, const char* message)
|
|
{
|
|
fprintf(stderr, "%s(%d,%d): %s: %s\n", name, location.begin.line + 1, location.begin.column + 1, type, message);
|
|
}
|
|
|
|
static void reportError(const char* name, const Luau::ParseError& error)
|
|
{
|
|
report(name, error.getLocation(), "SyntaxError", error.what());
|
|
}
|
|
|
|
static void reportError(const char* name, const Luau::CompileError& error)
|
|
{
|
|
report(name, error.getLocation(), "CompileError", error.what());
|
|
}
|
|
|
|
static std::string getCodegenAssembly(const char* name, const std::string& bytecode, Luau::CodeGen::AssemblyOptions options)
|
|
{
|
|
std::unique_ptr<lua_State, void (*)(lua_State*)> globalState(luaL_newstate(), lua_close);
|
|
lua_State* L = globalState.get();
|
|
|
|
if (luau_load(L, name, bytecode.data(), bytecode.size(), 0) == 0)
|
|
return Luau::CodeGen::getAssembly(L, -1, options);
|
|
|
|
fprintf(stderr, "Error loading bytecode %s\n", name);
|
|
return "";
|
|
}
|
|
|
|
static void annotateInstruction(void* context, std::string& text, int fid, int instpos)
|
|
{
|
|
Luau::BytecodeBuilder& bcb = *(Luau::BytecodeBuilder*)context;
|
|
|
|
bcb.annotateInstruction(text, fid, instpos);
|
|
}
|
|
|
|
struct CompileStats
|
|
{
|
|
size_t lines;
|
|
size_t bytecode;
|
|
size_t codegen;
|
|
|
|
double readTime;
|
|
double miscTime;
|
|
double parseTime;
|
|
double compileTime;
|
|
double codegenTime;
|
|
};
|
|
|
|
static double recordDeltaTime(double& timer)
|
|
{
|
|
double now = Luau::TimeTrace::getClock();
|
|
double delta = now - timer;
|
|
timer = now;
|
|
return delta;
|
|
}
|
|
|
|
static bool compileFile(const char* name, CompileFormat format, CompileStats& stats)
|
|
{
|
|
double currts = Luau::TimeTrace::getClock();
|
|
|
|
std::optional<std::string> source = readFile(name);
|
|
if (!source)
|
|
{
|
|
fprintf(stderr, "Error opening %s\n", name);
|
|
return false;
|
|
}
|
|
|
|
stats.readTime += recordDeltaTime(currts);
|
|
|
|
// NOTE: Normally, you should use Luau::compile or luau_compile (see lua_require as an example)
|
|
// This function is much more complicated because it supports many output human-readable formats through internal interfaces
|
|
|
|
try
|
|
{
|
|
Luau::BytecodeBuilder bcb;
|
|
|
|
Luau::CodeGen::AssemblyOptions options;
|
|
options.outputBinary = format == CompileFormat::CodegenNull;
|
|
|
|
if (!options.outputBinary)
|
|
{
|
|
options.includeAssembly = format != CompileFormat::CodegenIr;
|
|
options.includeIr = format != CompileFormat::CodegenAsm;
|
|
options.includeOutlinedCode = format == CompileFormat::CodegenVerbose;
|
|
}
|
|
|
|
options.annotator = annotateInstruction;
|
|
options.annotatorContext = &bcb;
|
|
|
|
if (format == CompileFormat::Text)
|
|
{
|
|
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Locals |
|
|
Luau::BytecodeBuilder::Dump_Remarks);
|
|
bcb.setDumpSource(*source);
|
|
}
|
|
else if (format == CompileFormat::Remarks)
|
|
{
|
|
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Remarks);
|
|
bcb.setDumpSource(*source);
|
|
}
|
|
else if (format == CompileFormat::Codegen || format == CompileFormat::CodegenAsm || format == CompileFormat::CodegenIr ||
|
|
format == CompileFormat::CodegenVerbose)
|
|
{
|
|
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source | Luau::BytecodeBuilder::Dump_Locals |
|
|
Luau::BytecodeBuilder::Dump_Remarks);
|
|
bcb.setDumpSource(*source);
|
|
}
|
|
|
|
stats.miscTime += recordDeltaTime(currts);
|
|
|
|
Luau::Allocator allocator;
|
|
Luau::AstNameTable names(allocator);
|
|
Luau::ParseResult result = Luau::Parser::parse(source->c_str(), source->size(), names, allocator);
|
|
|
|
if (!result.errors.empty())
|
|
throw Luau::ParseErrors(result.errors);
|
|
|
|
stats.lines += result.lines;
|
|
stats.parseTime += recordDeltaTime(currts);
|
|
|
|
Luau::compileOrThrow(bcb, result, names, copts());
|
|
stats.bytecode += bcb.getBytecode().size();
|
|
stats.compileTime += recordDeltaTime(currts);
|
|
|
|
switch (format)
|
|
{
|
|
case CompileFormat::Text:
|
|
printf("%s", bcb.dumpEverything().c_str());
|
|
break;
|
|
case CompileFormat::Remarks:
|
|
printf("%s", bcb.dumpSourceRemarks().c_str());
|
|
break;
|
|
case CompileFormat::Binary:
|
|
fwrite(bcb.getBytecode().data(), 1, bcb.getBytecode().size(), stdout);
|
|
break;
|
|
case CompileFormat::Codegen:
|
|
case CompileFormat::CodegenAsm:
|
|
case CompileFormat::CodegenIr:
|
|
case CompileFormat::CodegenVerbose:
|
|
printf("%s", getCodegenAssembly(name, bcb.getBytecode(), options).c_str());
|
|
break;
|
|
case CompileFormat::CodegenNull:
|
|
stats.codegen += getCodegenAssembly(name, bcb.getBytecode(), options).size();
|
|
stats.codegenTime += recordDeltaTime(currts);
|
|
break;
|
|
case CompileFormat::Null:
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
catch (Luau::ParseErrors& e)
|
|
{
|
|
for (auto& error : e.getErrors())
|
|
reportError(name, error);
|
|
return false;
|
|
}
|
|
catch (Luau::CompileError& e)
|
|
{
|
|
reportError(name, e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
static void displayHelp(const char* argv0)
|
|
{
|
|
printf("Usage: %s [--mode] [options] [file list]\n", argv0);
|
|
printf("\n");
|
|
printf("When mode and file list are omitted, an interactive REPL is started instead.\n");
|
|
printf("\n");
|
|
printf("Available modes:\n");
|
|
printf(" omitted: compile and run input files one by one\n");
|
|
printf(" --compile[=format]: compile input files and output resulting bytecode/assembly (binary, text, remarks, codegen)\n");
|
|
printf("\n");
|
|
printf("Available options:\n");
|
|
printf(" --coverage: collect code coverage while running the code and output results to coverage.out\n");
|
|
printf(" -h, --help: Display this usage message.\n");
|
|
printf(" -i, --interactive: Run an interactive REPL after executing the last script specified.\n");
|
|
printf(" -O<n>: compile with optimization level n (default 1, n should be between 0 and 2).\n");
|
|
printf(" -g<n>: compile with debug level n (default 1, n should be between 0 and 2).\n");
|
|
printf(" --profile[=N]: profile the code using N Hz sampling (default 10000) and output results to profile.out\n");
|
|
printf(" --timetrace: record compiler time tracing information into trace.json\n");
|
|
printf(" --codegen: execute code using native code generation\n");
|
|
}
|
|
|
|
static int assertionHandler(const char* expr, const char* file, int line, const char* function)
|
|
{
|
|
printf("%s(%d): ASSERTION FAILED: %s\n", file, line, expr);
|
|
return 1;
|
|
}
|
|
|
|
int replMain(int argc, char** argv)
|
|
{
|
|
Luau::assertHandler() = assertionHandler;
|
|
|
|
setLuauFlagsDefault();
|
|
|
|
CliMode mode = CliMode::Unknown;
|
|
CompileFormat compileFormat{};
|
|
int profile = 0;
|
|
bool coverage = false;
|
|
bool interactive = false;
|
|
bool codegenPerf = false;
|
|
|
|
// Set the mode if the user has explicitly specified one.
|
|
int argStart = 1;
|
|
if (argc >= 2 && strncmp(argv[1], "--compile", strlen("--compile")) == 0)
|
|
{
|
|
argStart++;
|
|
mode = CliMode::Compile;
|
|
if (strcmp(argv[1], "--compile") == 0)
|
|
{
|
|
compileFormat = CompileFormat::Text;
|
|
}
|
|
else if (strcmp(argv[1], "--compile=binary") == 0)
|
|
{
|
|
compileFormat = CompileFormat::Binary;
|
|
}
|
|
else if (strcmp(argv[1], "--compile=text") == 0)
|
|
{
|
|
compileFormat = CompileFormat::Text;
|
|
}
|
|
else if (strcmp(argv[1], "--compile=remarks") == 0)
|
|
{
|
|
compileFormat = CompileFormat::Remarks;
|
|
}
|
|
else if (strcmp(argv[1], "--compile=codegen") == 0)
|
|
{
|
|
compileFormat = CompileFormat::Codegen;
|
|
}
|
|
else if (strcmp(argv[1], "--compile=codegenasm") == 0)
|
|
{
|
|
compileFormat = CompileFormat::CodegenAsm;
|
|
}
|
|
else if (strcmp(argv[1], "--compile=codegenir") == 0)
|
|
{
|
|
compileFormat = CompileFormat::CodegenIr;
|
|
}
|
|
else if (strcmp(argv[1], "--compile=codegenverbose") == 0)
|
|
{
|
|
compileFormat = CompileFormat::CodegenVerbose;
|
|
}
|
|
else if (strcmp(argv[1], "--compile=codegennull") == 0)
|
|
{
|
|
compileFormat = CompileFormat::CodegenNull;
|
|
}
|
|
else if (strcmp(argv[1], "--compile=null") == 0)
|
|
{
|
|
compileFormat = CompileFormat::Null;
|
|
}
|
|
else
|
|
{
|
|
fprintf(stderr, "Error: Unrecognized value for '--compile' specified.\n");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
for (int i = argStart; i < argc; i++)
|
|
{
|
|
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0)
|
|
{
|
|
displayHelp(argv[0]);
|
|
return 0;
|
|
}
|
|
else if (strcmp(argv[i], "-i") == 0 || strcmp(argv[i], "--interactive") == 0)
|
|
{
|
|
interactive = true;
|
|
}
|
|
else if (strncmp(argv[i], "-O", 2) == 0)
|
|
{
|
|
int level = atoi(argv[i] + 2);
|
|
if (level < 0 || level > 2)
|
|
{
|
|
fprintf(stderr, "Error: Optimization level must be between 0 and 2 inclusive.\n");
|
|
return 1;
|
|
}
|
|
globalOptions.optimizationLevel = level;
|
|
}
|
|
else if (strncmp(argv[i], "-g", 2) == 0)
|
|
{
|
|
int level = atoi(argv[i] + 2);
|
|
if (level < 0 || level > 2)
|
|
{
|
|
fprintf(stderr, "Error: Debug level must be between 0 and 2 inclusive.\n");
|
|
return 1;
|
|
}
|
|
globalOptions.debugLevel = level;
|
|
}
|
|
else if (strcmp(argv[i], "--profile") == 0)
|
|
{
|
|
profile = 10000; // default to 10 KHz
|
|
}
|
|
else if (strncmp(argv[i], "--profile=", 10) == 0)
|
|
{
|
|
profile = atoi(argv[i] + 10);
|
|
}
|
|
else if (strcmp(argv[i], "--codegen") == 0)
|
|
{
|
|
codegen = true;
|
|
}
|
|
else if (strcmp(argv[i], "--codegen-perf") == 0)
|
|
{
|
|
codegen = true;
|
|
codegenPerf = true;
|
|
}
|
|
else if (strcmp(argv[i], "--coverage") == 0)
|
|
{
|
|
coverage = true;
|
|
}
|
|
else if (strcmp(argv[i], "--timetrace") == 0)
|
|
{
|
|
FFlag::DebugLuauTimeTracing.value = true;
|
|
}
|
|
else if (strncmp(argv[i], "--fflags=", 9) == 0)
|
|
{
|
|
setLuauFlags(argv[i] + 9);
|
|
}
|
|
else if (argv[i][0] == '-')
|
|
{
|
|
fprintf(stderr, "Error: Unrecognized option '%s'.\n\n", argv[i]);
|
|
displayHelp(argv[0]);
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
#if !defined(LUAU_ENABLE_TIME_TRACE)
|
|
if (FFlag::DebugLuauTimeTracing)
|
|
{
|
|
fprintf(stderr, "To run with --timetrace, Luau has to be built with LUAU_ENABLE_TIME_TRACE enabled\n");
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
#if !LUA_CUSTOM_EXECUTION
|
|
if (codegen)
|
|
{
|
|
fprintf(stderr, "To run with --codegen, Luau has to be built with LUA_CUSTOM_EXECUTION enabled\n");
|
|
return 1;
|
|
}
|
|
#endif
|
|
|
|
if (codegenPerf)
|
|
{
|
|
#if __linux__
|
|
char path[128];
|
|
snprintf(path, sizeof(path), "/tmp/perf-%d.map", getpid());
|
|
|
|
// note, there's no need to close the log explicitly as it will be closed when the process exits
|
|
FILE* codegenPerfLog = fopen(path, "w");
|
|
|
|
Luau::CodeGen::setPerfLog(codegenPerfLog, [](void* context, uintptr_t addr, unsigned size, const char* symbol) {
|
|
fprintf(static_cast<FILE*>(context), "%016lx %08x %s\n", long(addr), size, symbol);
|
|
});
|
|
#else
|
|
fprintf(stderr, "--codegen-perf option is only supported on Linux\n");
|
|
return 1;
|
|
#endif
|
|
}
|
|
|
|
const std::vector<std::string> files = getSourceFiles(argc, argv);
|
|
if (mode == CliMode::Unknown)
|
|
{
|
|
mode = files.empty() ? CliMode::Repl : CliMode::RunSourceFiles;
|
|
}
|
|
|
|
if (mode != CliMode::Compile && codegen && !Luau::CodeGen::isSupported())
|
|
{
|
|
fprintf(stderr, "Cannot enable --codegen, native code generation is not supported in current configuration\n");
|
|
return 1;
|
|
}
|
|
|
|
switch (mode)
|
|
{
|
|
case CliMode::Compile:
|
|
{
|
|
#ifdef _WIN32
|
|
if (compileFormat == CompileFormat::Binary)
|
|
_setmode(_fileno(stdout), _O_BINARY);
|
|
#endif
|
|
|
|
CompileStats stats = {};
|
|
int failed = 0;
|
|
|
|
for (const std::string& path : files)
|
|
failed += !compileFile(path.c_str(), compileFormat, stats);
|
|
|
|
if (compileFormat == CompileFormat::Null)
|
|
printf("Compiled %d KLOC into %d KB bytecode (read %.2fs, parse %.2fs, compile %.2fs)\n", int(stats.lines / 1000),
|
|
int(stats.bytecode / 1024), stats.readTime, stats.parseTime, stats.compileTime);
|
|
else if (compileFormat == CompileFormat::CodegenNull)
|
|
printf("Compiled %d KLOC into %d KB bytecode => %d KB native code (%.2fx) (read %.2fs, parse %.2fs, compile %.2fs, codegen %.2fs)\n",
|
|
int(stats.lines / 1000), int(stats.bytecode / 1024), int(stats.codegen / 1024),
|
|
stats.bytecode == 0 ? 0.0 : double(stats.codegen) / double(stats.bytecode), stats.readTime, stats.parseTime, stats.compileTime,
|
|
stats.codegenTime);
|
|
|
|
return failed ? 1 : 0;
|
|
}
|
|
case CliMode::Repl:
|
|
{
|
|
runRepl();
|
|
return 0;
|
|
}
|
|
case CliMode::RunSourceFiles:
|
|
{
|
|
std::unique_ptr<lua_State, void (*)(lua_State*)> globalState(luaL_newstate(), lua_close);
|
|
lua_State* L = globalState.get();
|
|
|
|
setupState(L);
|
|
|
|
if (profile)
|
|
profilerStart(L, profile);
|
|
|
|
if (coverage)
|
|
coverageInit(L);
|
|
|
|
int failed = 0;
|
|
|
|
for (size_t i = 0; i < files.size(); ++i)
|
|
{
|
|
bool isLastFile = i == files.size() - 1;
|
|
failed += !runFile(files[i].c_str(), L, interactive && isLastFile);
|
|
}
|
|
|
|
if (profile)
|
|
{
|
|
profilerStop();
|
|
profilerDump("profile.out");
|
|
}
|
|
|
|
if (coverage)
|
|
coverageDump("coverage.out");
|
|
|
|
return failed ? 1 : 0;
|
|
}
|
|
case CliMode::Unknown:
|
|
default:
|
|
LUAU_ASSERT(!"Unhandled cli mode.");
|
|
return 1;
|
|
}
|
|
}
|