// 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/Compiler.h" #include "Luau/BytecodeBuilder.h" #include "Luau/Parser.h" #include "FileUtils.h" #include "Profiler.h" #include "Coverage.h" #include "linenoise.hpp" #include #ifdef _WIN32 #include #include #endif enum class CompileFormat { Text, Binary }; static Luau::CompileOptions copts() { Luau::CompileOptions result = {}; result.optimizationLevel = 1; result.debugLevel = 1; 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)) return finishrequire(L); lua_pop(L, 1); std::optional 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 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 (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; stack of L is MODULES thread lua_xmove(ML, L, 1); lua_pushvalue(L, -1); lua_setfield(L, -4, name.c_str()); 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'"); } static void setupState(lua_State* L) { luaL_openlibs(L); static const luaL_Reg funcs[] = { {"loadstring", lua_loadstring}, {"require", lua_require}, {"collectgarbage", lua_collectgarbage}, {NULL, NULL}, }; lua_pushvalue(L, LUA_GLOBALSINDEX); luaL_register(L, NULL, funcs); lua_pop(L, 1); luaL_sandbox(L); } static 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; } 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, "print"); lua_insert(T, 1); lua_pcall(T, n, 0, 0); } } 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); fprintf(stdout, "%s", error.c_str()); } lua_pop(L, 1); return std::string(); } static void completeIndexer(lua_State* L, const char* editBuffer, size_t start, std::vector& completions) { std::string_view lookup = editBuffer + start; for (;;) { size_t dot = lookup.find('.'); std::string_view prefix = lookup.substr(0, dot); if (dot == std::string_view::npos) { // table, key lua_pushnil(L); while (lua_next(L, -2) != 0) { if (lua_type(L, -2) == LUA_TSTRING) { // table, key, value std::string_view key = lua_tostring(L, -2); if (!key.empty() && Luau::startsWith(key, prefix)) completions.push_back(editBuffer + std::string(key.substr(prefix.size()))); } lua_pop(L, 1); } break; } else { // find the key in the table lua_pushlstring(L, prefix.data(), prefix.size()); lua_rawget(L, -2); lua_remove(L, -2); if (!lua_istable(L, -1)) break; lookup.remove_prefix(dot + 1); } } lua_pop(L, 1); } static void completeRepl(lua_State* L, const char* editBuffer, std::vector& completions) { size_t start = strlen(editBuffer); while (start > 0 && (isalnum(editBuffer[start - 1]) || editBuffer[start - 1] == '.' || editBuffer[start - 1] == '_')) start--; // look the value up in current global table first lua_pushvalue(L, LUA_GLOBALSINDEX); completeIndexer(L, editBuffer, start, completions); // and in actual global table after that lua_getglobal(L, "_G"); completeIndexer(L, editBuffer, start, completions); } struct LinenoiseScopedHistory { LinenoiseScopedHistory() { const std::string name(".luau_history"); if (const char* home = getenv("HOME")) { historyFilepath = joinPaths(home, name); } else if (const char* userProfile = getenv("USERPROFILE")) { historyFilepath = joinPaths(userProfile, name); } if (!historyFilepath.empty()) linenoise::LoadHistory(historyFilepath.c_str()); } ~LinenoiseScopedHistory() { if (!historyFilepath.empty()) linenoise::SaveHistory(historyFilepath.c_str()); } std::string historyFilepath; }; static void runRepl() { std::unique_ptr globalState(luaL_newstate(), lua_close); lua_State* L = globalState.get(); setupState(L); luaL_sandboxthread(L); linenoise::SetCompletionCallback([L](const char* editBuffer, std::vector& completions) { completeRepl(L, editBuffer, completions); }); std::string buffer; LinenoiseScopedHistory scopedHistory; for (;;) { bool quit = false; std::string line = linenoise::Readline(buffer.empty() ? "> " : ">> ", quit); if (quit) break; if (buffer.empty() && runCode(L, std::string("return ") + line) == std::string()) { linenoise::AddHistory(line.c_str()); continue; } buffer += line; buffer += " "; // linenoise doesn't work very well with multiline history entries std::string error = runCode(L, buffer); if (error.length() >= 5 && error.compare(error.length() - 5, 5, "") == 0) { continue; } if (error.length()) { fprintf(stdout, "%s\n", error.c_str()); } linenoise::AddHistory(buffer.c_str()); buffer.clear(); } } static bool runFile(const char* name, lua_State* GL) { std::optional 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 (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()); } 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 bool compileFile(const char* name, CompileFormat format) { std::optional source = readFile(name); if (!source) { fprintf(stderr, "Error opening %s\n", name); return false; } try { Luau::BytecodeBuilder bcb; if (format == CompileFormat::Text) { bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code | Luau::BytecodeBuilder::Dump_Source); bcb.setDumpSource(*source); } Luau::compileOrThrow(bcb, *source); switch (format) { case CompileFormat::Text: printf("%s", bcb.dumpEverything().c_str()); break; case CompileFormat::Binary: fwrite(bcb.getBytecode().data(), 1, bcb.getBytecode().size(), stdout); 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 formatted bytecode (binary or text)\n"); printf("\n"); printf("Available options:\n"); printf(" --profile[=N]: profile the code using N Hz sampling (default 10000) and output results to profile.out\n"); printf(" --coverage: collect code coverage while running the code and output results to coverage.out\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 main(int argc, char** argv) { Luau::assertHandler() = assertionHandler; for (Luau::FValue* flag = Luau::FValue::list; flag; flag = flag->next) if (strncmp(flag->name, "Luau", 4) == 0) flag->value = true; if (argc == 1) { runRepl(); return 0; } if (argc >= 2 && strcmp(argv[1], "--help") == 0) { displayHelp(argv[0]); return 0; } if (argc >= 2 && strncmp(argv[1], "--compile", strlen("--compile")) == 0) { CompileFormat format = CompileFormat::Text; if (strcmp(argv[1], "--compile=binary") == 0) format = CompileFormat::Binary; #ifdef _WIN32 if (format == CompileFormat::Binary) _setmode(_fileno(stdout), _O_BINARY); #endif std::vector files = getSourceFiles(argc, argv); int failed = 0; for (const std::string& path : files) failed += !compileFile(path.c_str(), format); return failed; } { std::unique_ptr globalState(luaL_newstate(), lua_close); lua_State* L = globalState.get(); setupState(L); int profile = 0; bool coverage = false; for (int i = 1; i < argc; ++i) { if (argv[i][0] != '-') continue; 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], "--coverage") == 0) coverage = true; } if (profile) profilerStart(L, profile); if (coverage) coverageInit(L); std::vector files = getSourceFiles(argc, argv); int failed = 0; for (const std::string& path : files) failed += !runFile(path.c_str(), L); if (profile) { profilerStop(); profilerDump("profile.out"); } if (coverage) coverageDump("coverage.out"); return failed; } }