Merge branch 'master' into merge

This commit is contained in:
Arseny Kapoulkine 2021-11-11 18:21:14 -08:00
commit ce0bbdda59
28 changed files with 483 additions and 79 deletions

View File

@ -1,5 +1,5 @@
blank_issues_enabled: false blank_issues_enabled: false
contact_links: contact_links:
- name: Help and support - name: Questions
url: https://github.com/Roblox/luau/discussions url: https://github.com/Roblox/luau/discussions
about: Please use GitHub Discussions if you have questions or need help. about: Please use GitHub Discussions if you have questions or need help.

1
.gitignore vendored
View File

@ -5,3 +5,4 @@
^default.prof* ^default.prof*
^fuzz-* ^fuzz-*
^luau$ ^luau$
/.vs

View File

@ -496,7 +496,7 @@ static bool canSuggestInferredType(ScopePtr scope, TypeId ty)
return false; return false;
// No syntax for unnamed tables with a metatable // No syntax for unnamed tables with a metatable
if (const MetatableTypeVar* mtv = get<MetatableTypeVar>(ty)) if (get<MetatableTypeVar>(ty))
return false; return false;
if (const TableTypeVar* ttv = get<TableTypeVar>(ty)) if (const TableTypeVar* ttv = get<TableTypeVar>(ty))
@ -688,7 +688,7 @@ static std::optional<bool> functionIsExpectedAt(const Module& module, AstNode* n
TypeId expectedType = follow(*it); TypeId expectedType = follow(*it);
if (const FunctionTypeVar* ftv = get<FunctionTypeVar>(expectedType)) if (get<FunctionTypeVar>(expectedType))
return true; return true;
if (const IntersectionTypeVar* itv = get<IntersectionTypeVar>(expectedType)) if (const IntersectionTypeVar* itv = get<IntersectionTypeVar>(expectedType))

View File

@ -2129,7 +2129,7 @@ TypeId TypeChecker::checkRelationalOperation(
if (!isNonstrictMode() && !isOrOp) if (!isNonstrictMode() && !isOrOp)
return ty; return ty;
if (auto i = get<UnionTypeVar>(ty)) if (get<UnionTypeVar>(ty))
{ {
std::optional<TypeId> cleaned = tryStripUnionFromNil(ty); std::optional<TypeId> cleaned = tryStripUnionFromNil(ty);

View File

@ -216,7 +216,7 @@ bool finite(TypePackId tp)
if (auto pack = get<TypePack>(tp)) if (auto pack = get<TypePack>(tp))
return pack->tail ? finite(*pack->tail) : true; return pack->tail ? finite(*pack->tail) : true;
if (auto pack = get<VariadicTypePack>(tp)) if (get<VariadicTypePack>(tp))
return false; return false;
return true; return true;

View File

@ -749,9 +749,9 @@ void StateDot::visitChild(TypeId ty, int parentIndex, const char* linkName)
if (opts.duplicatePrimitives && canDuplicatePrimitive(ty)) if (opts.duplicatePrimitives && canDuplicatePrimitive(ty))
{ {
if (const PrimitiveTypeVar* ptv = get<PrimitiveTypeVar>(ty)) if (get<PrimitiveTypeVar>(ty))
formatAppend(result, "n%d [label=\"%s\"];\n", index, toStringDetailed(ty, {}).name.c_str()); formatAppend(result, "n%d [label=\"%s\"];\n", index, toStringDetailed(ty, {}).name.c_str());
else if (const AnyTypeVar* atv = get<AnyTypeVar>(ty)) else if (get<AnyTypeVar>(ty))
formatAppend(result, "n%d [label=\"any\"];\n", index); formatAppend(result, "n%d [label=\"any\"];\n", index);
} }
else else
@ -902,19 +902,19 @@ void StateDot::visitChildren(TypeId ty, int index)
finishNodeLabel(ty); finishNodeLabel(ty);
finishNode(); finishNode();
} }
else if (const AnyTypeVar* atv = get<AnyTypeVar>(ty)) else if (get<AnyTypeVar>(ty))
{ {
formatAppend(result, "AnyTypeVar %d", index); formatAppend(result, "AnyTypeVar %d", index);
finishNodeLabel(ty); finishNodeLabel(ty);
finishNode(); finishNode();
} }
else if (const PrimitiveTypeVar* ptv = get<PrimitiveTypeVar>(ty)) else if (get<PrimitiveTypeVar>(ty))
{ {
formatAppend(result, "PrimitiveTypeVar %s", toStringDetailed(ty, {}).name.c_str()); formatAppend(result, "PrimitiveTypeVar %s", toStringDetailed(ty, {}).name.c_str());
finishNodeLabel(ty); finishNodeLabel(ty);
finishNode(); finishNode();
} }
else if (const ErrorTypeVar* etv = get<ErrorTypeVar>(ty)) else if (get<ErrorTypeVar>(ty))
{ {
formatAppend(result, "ErrorTypeVar %d", index); formatAppend(result, "ErrorTypeVar %d", index);
finishNodeLabel(ty); finishNodeLabel(ty);
@ -994,7 +994,7 @@ void StateDot::visitChildren(TypePackId tp, int index)
finishNodeLabel(tp); finishNodeLabel(tp);
finishNode(); finishNode();
} }
else if (const Unifiable::Error* etp = get<Unifiable::Error>(tp)) else if (get<Unifiable::Error>(tp))
{ {
formatAppend(result, "ErrorTypePack %d", index); formatAppend(result, "ErrorTypePack %d", index);
finishNodeLabel(tp); finishNodeLabel(tp);

View File

@ -2379,15 +2379,12 @@ AstArray<AstTypeOrPack> Parser::parseTypeParams()
Lexeme begin = lexer.current(); Lexeme begin = lexer.current();
nextLexeme(); nextLexeme();
bool seenPack = false;
while (true) while (true)
{ {
if (FFlag::LuauParseTypePackTypeParameters) if (FFlag::LuauParseTypePackTypeParameters)
{ {
if (shouldParseTypePackAnnotation(lexer)) if (shouldParseTypePackAnnotation(lexer))
{ {
seenPack = true;
auto typePack = parseTypePackAnnotation(); auto typePack = parseTypePackAnnotation();
if (FFlag::LuauTypeAliasPacks) // Type packs are recorded only is we can handle them if (FFlag::LuauTypeAliasPacks) // Type packs are recorded only is we can handle them
@ -2399,8 +2396,6 @@ AstArray<AstTypeOrPack> Parser::parseTypeParams()
if (typePack) if (typePack)
{ {
seenPack = true;
if (FFlag::LuauTypeAliasPacks) // Type packs are recorded only is we can handle them if (FFlag::LuauTypeAliasPacks) // Type packs are recorded only is we can handle them
parameters.push_back({{}, typePack}); parameters.push_back({{}, typePack});
} }

View File

@ -34,8 +34,10 @@ static void report(ReportFormat format, const char* name, const Luau::Location&
} }
} }
static void reportError(ReportFormat format, const char* name, const Luau::TypeError& error) static void reportError(ReportFormat format, const Luau::TypeError& error)
{ {
const char* name = error.moduleName.c_str();
if (const Luau::SyntaxError* syntaxError = Luau::get_if<Luau::SyntaxError>(&error.data)) if (const Luau::SyntaxError* syntaxError = Luau::get_if<Luau::SyntaxError>(&error.data))
report(format, name, error.location, "SyntaxError", syntaxError->message.c_str()); report(format, name, error.location, "SyntaxError", syntaxError->message.c_str());
else else
@ -49,7 +51,10 @@ static void reportWarning(ReportFormat format, const char* name, const Luau::Lin
static bool analyzeFile(Luau::Frontend& frontend, const char* name, ReportFormat format, bool annotate) static bool analyzeFile(Luau::Frontend& frontend, const char* name, ReportFormat format, bool annotate)
{ {
Luau::CheckResult cr = frontend.check(name); Luau::CheckResult cr;
if (frontend.isDirty(name))
cr = frontend.check(name);
if (!frontend.getSourceModule(name)) if (!frontend.getSourceModule(name))
{ {
@ -58,7 +63,7 @@ static bool analyzeFile(Luau::Frontend& frontend, const char* name, ReportFormat
} }
for (auto& error : cr.errors) for (auto& error : cr.errors)
reportError(format, name, error); reportError(format, error);
Luau::LintResult lr = frontend.lint(name); Luau::LintResult lr = frontend.lint(name);
@ -115,7 +120,12 @@ struct CliFileResolver : Luau::FileResolver
{ {
if (Luau::AstExprConstantString* expr = node->as<Luau::AstExprConstantString>()) if (Luau::AstExprConstantString* expr = node->as<Luau::AstExprConstantString>())
{ {
Luau::ModuleName name = std::string(expr->value.data, expr->value.size) + ".lua"; Luau::ModuleName name = std::string(expr->value.data, expr->value.size) + ".luau";
if (!moduleExists(name))
{
// fall back to .lua if a module with .luau doesn't exist
name = std::string(expr->value.data, expr->value.size) + ".lua";
}
return {{name}}; return {{name}};
} }
@ -236,8 +246,15 @@ int main(int argc, char** argv)
if (isDirectory(argv[i])) if (isDirectory(argv[i]))
{ {
traverseDirectory(argv[i], [&](const std::string& name) { traverseDirectory(argv[i], [&](const std::string& name) {
if (name.length() > 4 && name.rfind(".lua") == name.length() - 4) // Look for .luau first and if absent, fall back to .lua
if (name.length() > 5 && name.rfind(".luau") == name.length() - 5)
{
failed += !analyzeFile(frontend, name.c_str(), format, annotate); failed += !analyzeFile(frontend, name.c_str(), format, annotate);
}
else if (name.length() > 4 && name.rfind(".lua") == name.length() - 4)
{
failed += !analyzeFile(frontend, name.c_str(), format, annotate);
}
}); });
} }
else else
@ -256,5 +273,3 @@ int main(int argc, char** argv)
return (format == ReportFormat::Luacheck) ? 0 : failed; return (format == ReportFormat::Luacheck) ? 0 : failed;
} }

View File

@ -13,6 +13,17 @@
#include <memory> #include <memory>
#ifdef _WIN32
#include <io.h>
#include <fcntl.h>
#endif
enum class CompileFormat
{
Default,
Binary
};
static int lua_loadstring(lua_State* L) static int lua_loadstring(lua_State* L)
{ {
size_t l = 0; size_t l = 0;
@ -51,9 +62,13 @@ static int lua_require(lua_State* L)
return finishrequire(L); return finishrequire(L);
lua_pop(L, 1); lua_pop(L, 1);
std::optional<std::string> source = readFile(name + ".lua"); std::optional<std::string> source = readFile(name + ".luau");
if (!source) if (!source)
luaL_argerrorL(L, 1, ("error loading " + name).c_str()); {
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 // module needs to run in a new thread, isolated from the rest
lua_State* GL = lua_mainthread(L); lua_State* GL = lua_mainthread(L);
@ -183,6 +198,11 @@ static std::string runCode(lua_State* L, const std::string& source)
error += "\nstack backtrace:\n"; error += "\nstack backtrace:\n";
error += lua_debugtrace(T); error += lua_debugtrace(T);
#ifdef __EMSCRIPTEN__
// nicer formatting for errors in web repl
error = "Error:" + error;
#endif
fprintf(stdout, "%s", error.c_str()); fprintf(stdout, "%s", error.c_str());
} }
@ -190,6 +210,44 @@ static std::string runCode(lua_State* L, const std::string& source)
return std::string(); return std::string();
} }
#ifdef __EMSCRIPTEN__
extern "C"
{
const char* executeScript(const char* source)
{
// static string for caching result (prevents dangling ptr on function exit)
static std::string result;
// setup flags
for (Luau::FValue<bool>* flag = Luau::FValue<bool>::list; flag; flag = flag->next)
if (strncmp(flag->name, "Luau", 4) == 0)
flag->value = true;
// create new state
std::unique_ptr<lua_State, void (*)(lua_State*)> globalState(luaL_newstate(), lua_close);
lua_State* L = globalState.get();
// setup state
setupState(L);
// sandbox thread
luaL_sandboxthread(L);
// run code + collect error
std::string error = runCode(L, source);
result = error;
if (error.length())
{
return result.c_str();
}
return NULL;
}
}
#endif
// Excluded from emscripten compilation to avoid -Wunused-function errors.
#ifndef __EMSCRIPTEN__
static void completeIndexer(lua_State* L, const char* editBuffer, size_t start, std::vector<std::string>& completions) static void completeIndexer(lua_State* L, const char* editBuffer, size_t start, std::vector<std::string>& completions)
{ {
std::string_view lookup = editBuffer + start; std::string_view lookup = editBuffer + start;
@ -366,7 +424,7 @@ static void reportError(const char* name, const Luau::CompileError& error)
report(name, error.getLocation(), "CompileError", error.what()); report(name, error.getLocation(), "CompileError", error.what());
} }
static bool compileFile(const char* name) static bool compileFile(const char* name, CompileFormat format)
{ {
std::optional<std::string> source = readFile(name); std::optional<std::string> source = readFile(name);
if (!source) if (!source)
@ -383,7 +441,15 @@ static bool compileFile(const char* name)
Luau::compileOrThrow(bcb, *source); Luau::compileOrThrow(bcb, *source);
printf("%s", bcb.dumpEverything().c_str()); switch (format)
{
case CompileFormat::Default:
printf("%s", bcb.dumpEverything().c_str());
break;
case CompileFormat::Binary:
fwrite(bcb.getBytecode().data(), 1, bcb.getBytecode().size(), stdout);
break;
}
return true; return true;
} }
@ -408,7 +474,7 @@ static void displayHelp(const char* argv0)
printf("\n"); printf("\n");
printf("Available modes:\n"); printf("Available modes:\n");
printf(" omitted: compile and run input files one by one\n"); printf(" omitted: compile and run input files one by one\n");
printf(" --compile: compile input files and output resulting bytecode\n"); printf(" --compile[=format]: compile input files and output resulting formatted bytecode (binary or text)\n");
printf("\n"); printf("\n");
printf("Available options:\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(" --profile[=N]: profile the code using N Hz sampling (default 10000) and output results to profile.out\n");
@ -440,8 +506,19 @@ int main(int argc, char** argv)
return 0; return 0;
} }
if (argc >= 2 && strcmp(argv[1], "--compile") == 0)
if (argc >= 2 && strncmp(argv[1], "--compile", strlen("--compile")) == 0)
{ {
CompileFormat format = CompileFormat::Default;
if (strcmp(argv[1], "--compile=binary") == 0)
format = CompileFormat::Binary;
#ifdef _WIN32
if (format == CompileFormat::Binary)
_setmode(_fileno(stdout), _O_BINARY);
#endif
int failed = 0; int failed = 0;
for (int i = 2; i < argc; ++i) for (int i = 2; i < argc; ++i)
@ -452,13 +529,15 @@ int main(int argc, char** argv)
if (isDirectory(argv[i])) if (isDirectory(argv[i]))
{ {
traverseDirectory(argv[i], [&](const std::string& name) { traverseDirectory(argv[i], [&](const std::string& name) {
if (name.length() > 4 && name.rfind(".lua") == name.length() - 4) if (name.length() > 5 && name.rfind(".luau") == name.length() - 5)
failed += !compileFile(name.c_str()); failed += !compileFile(name.c_str(), format);
else if (name.length() > 4 && name.rfind(".lua") == name.length() - 4)
failed += !compileFile(name.c_str(), format);
}); });
} }
else else
{ {
failed += !compileFile(argv[i]); failed += !compileFile(argv[i], format);
} }
} }
@ -511,5 +590,4 @@ int main(int argc, char** argv)
return failed; return failed;
} }
} }
#endif

View File

@ -17,17 +17,26 @@ add_library(Luau.VM STATIC)
if(LUAU_BUILD_CLI) if(LUAU_BUILD_CLI)
add_executable(Luau.Repl.CLI) add_executable(Luau.Repl.CLI)
add_executable(Luau.Analyze.CLI) if(NOT EMSCRIPTEN)
add_executable(Luau.Analyze.CLI)
else()
# add -fexceptions for emscripten to allow exceptions to be caught in C++
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fexceptions")
endif()
# This also adds target `name` on Linux/macOS and `name.exe` on Windows # This also adds target `name` on Linux/macOS and `name.exe` on Windows
set_target_properties(Luau.Repl.CLI PROPERTIES OUTPUT_NAME luau) set_target_properties(Luau.Repl.CLI PROPERTIES OUTPUT_NAME luau)
set_target_properties(Luau.Analyze.CLI PROPERTIES OUTPUT_NAME luau-analyze)
if(NOT EMSCRIPTEN)
set_target_properties(Luau.Analyze.CLI PROPERTIES OUTPUT_NAME luau-analyze)
endif()
endif() endif()
if(LUAU_BUILD_TESTS) if(LUAU_BUILD_TESTS AND NOT EMSCRIPTEN)
add_executable(Luau.UnitTest) add_executable(Luau.UnitTest)
add_executable(Luau.Conformance) add_executable(Luau.Conformance)
endif() endif()
include(Sources.cmake) include(Sources.cmake)
target_compile_features(Luau.Ast PUBLIC cxx_std_17) target_compile_features(Luau.Ast PUBLIC cxx_std_17)
@ -53,10 +62,6 @@ if(MSVC)
else() else()
list(APPEND LUAU_OPTIONS -Wall) # All warnings list(APPEND LUAU_OPTIONS -Wall) # All warnings
list(APPEND LUAU_OPTIONS -Werror) # Warnings are errors list(APPEND LUAU_OPTIONS -Werror) # Warnings are errors
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
list(APPEND LUAU_OPTIONS -Wno-unused) # GCC considers variables declared/checked in if() as unused
endif()
endif() endif()
target_compile_options(Luau.Ast PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Ast PRIVATE ${LUAU_OPTIONS})
@ -65,7 +70,10 @@ target_compile_options(Luau.VM PRIVATE ${LUAU_OPTIONS})
if(LUAU_BUILD_CLI) if(LUAU_BUILD_CLI)
target_compile_options(Luau.Repl.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Repl.CLI PRIVATE ${LUAU_OPTIONS})
target_compile_options(Luau.Analyze.CLI PRIVATE ${LUAU_OPTIONS})
if(NOT EMSCRIPTEN)
target_compile_options(Luau.Analyze.CLI PRIVATE ${LUAU_OPTIONS})
endif()
target_include_directories(Luau.Repl.CLI PRIVATE extern) target_include_directories(Luau.Repl.CLI PRIVATE extern)
target_link_libraries(Luau.Repl.CLI PRIVATE Luau.Compiler Luau.VM) target_link_libraries(Luau.Repl.CLI PRIVATE Luau.Compiler Luau.VM)
@ -74,10 +82,20 @@ if(LUAU_BUILD_CLI)
target_link_libraries(Luau.Repl.CLI PRIVATE pthread) target_link_libraries(Luau.Repl.CLI PRIVATE pthread)
endif() endif()
target_link_libraries(Luau.Analyze.CLI PRIVATE Luau.Analysis) if(NOT EMSCRIPTEN)
target_link_libraries(Luau.Analyze.CLI PRIVATE Luau.Analysis)
endif()
if(EMSCRIPTEN)
# declare exported functions to emscripten
target_link_options(Luau.Repl.CLI PRIVATE -sEXPORTED_FUNCTIONS=['_executeScript'] -sEXPORTED_RUNTIME_METHODS=['ccall','cwrap'] -fexceptions)
# custom output directory for wasm + js file
set_target_properties(Luau.Repl.CLI PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/docs/assets/luau)
endif()
endif() endif()
if(LUAU_BUILD_TESTS) if(LUAU_BUILD_TESTS AND NOT EMSCRIPTEN)
target_compile_options(Luau.UnitTest PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.UnitTest PRIVATE ${LUAU_OPTIONS})
target_include_directories(Luau.UnitTest PRIVATE extern) target_include_directories(Luau.UnitTest PRIVATE extern)
target_link_libraries(Luau.UnitTest PRIVATE Luau.Analysis Luau.Compiler) target_link_libraries(Luau.UnitTest PRIVATE Luau.Analysis Luau.Compiler)

View File

@ -36,6 +36,10 @@ struct CompileOptions
// global builtin to construct vectors; disabled by default // global builtin to construct vectors; disabled by default
const char* vectorLib = nullptr; const char* vectorLib = nullptr;
const char* vectorCtor = nullptr; const char* vectorCtor = nullptr;
// array of globals that are mutable; disables the import optimization for fields accessed through them
// use NULL to end the array
const char** mutableGlobals = nullptr;
}; };
class CompileError : public std::exception class CompileError : public std::exception

View File

@ -13,6 +13,7 @@
LUAU_FASTFLAGVARIABLE(LuauPreloadClosures, false) LUAU_FASTFLAGVARIABLE(LuauPreloadClosures, false)
LUAU_FASTFLAGVARIABLE(LuauPreloadClosuresFenv, false) LUAU_FASTFLAGVARIABLE(LuauPreloadClosuresFenv, false)
LUAU_FASTFLAGVARIABLE(LuauPreloadClosuresUpval, false) LUAU_FASTFLAGVARIABLE(LuauPreloadClosuresUpval, false)
LUAU_FASTFLAGVARIABLE(LuauGenericSpecialGlobals, false)
LUAU_FASTFLAG(LuauIfElseExpressionBaseSupport) LUAU_FASTFLAG(LuauIfElseExpressionBaseSupport)
namespace Luau namespace Luau
@ -1277,7 +1278,7 @@ struct Compiler
{ {
const Global* global = globals.find(expr->name); const Global* global = globals.find(expr->name);
return options.optimizationLevel >= 1 && (!global || (!global->written && !global->special)); return options.optimizationLevel >= 1 && (!global || (!global->written && !global->writable));
} }
void compileExprIndexName(AstExprIndexName* expr, uint8_t target) void compileExprIndexName(AstExprIndexName* expr, uint8_t target)
@ -2465,9 +2466,10 @@ struct Compiler
} }
else if (node->is<AstStatBreak>()) else if (node->is<AstStatBreak>())
{ {
LUAU_ASSERT(!loops.empty());
// before exiting out of the loop, we need to close all local variables that were captured in closures since loop start // before exiting out of the loop, we need to close all local variables that were captured in closures since loop start
// normally they are closed by the enclosing blocks, including the loop block, but we're skipping that here // normally they are closed by the enclosing blocks, including the loop block, but we're skipping that here
LUAU_ASSERT(!loops.empty());
closeLocals(loops.back().localOffset); closeLocals(loops.back().localOffset);
size_t label = bytecode.emitLabel(); size_t label = bytecode.emitLabel();
@ -2478,12 +2480,13 @@ struct Compiler
} }
else if (AstStatContinue* stat = node->as<AstStatContinue>()) else if (AstStatContinue* stat = node->as<AstStatContinue>())
{ {
LUAU_ASSERT(!loops.empty());
if (loops.back().untilCondition) if (loops.back().untilCondition)
validateContinueUntil(stat, loops.back().untilCondition); validateContinueUntil(stat, loops.back().untilCondition);
// before continuing, we need to close all local variables that were captured in closures since loop start // before continuing, we need to close all local variables that were captured in closures since loop start
// normally they are closed by the enclosing blocks, including the loop block, but we're skipping that here // normally they are closed by the enclosing blocks, including the loop block, but we're skipping that here
LUAU_ASSERT(!loops.empty());
closeLocals(loops.back().localOffset); closeLocals(loops.back().localOffset);
size_t label = bytecode.emitLabel(); size_t label = bytecode.emitLabel();
@ -2900,6 +2903,11 @@ struct Compiler
break; break;
case AstExprUnary::Len: case AstExprUnary::Len:
if (arg.type == Constant::Type_String)
{
result.type = Constant::Type_Number;
result.valueNumber = double(arg.valueString.size);
}
break; break;
default: default:
@ -3440,7 +3448,7 @@ struct Compiler
struct Global struct Global
{ {
bool special = false; bool writable = false;
bool written = false; bool written = false;
}; };
@ -3498,7 +3506,7 @@ struct Compiler
{ {
Global* g = globals.find(object->name); Global* g = globals.find(object->name);
return !g || (!g->special && !g->written) ? Builtin{object->name, expr->index} : Builtin(); return !g || (!g->writable && !g->written) ? Builtin{object->name, expr->index} : Builtin();
} }
else else
{ {
@ -3696,13 +3704,26 @@ void compileOrThrow(BytecodeBuilder& bytecode, AstStatBlock* root, const AstName
Compiler compiler(bytecode, options); Compiler compiler(bytecode, options);
// since access to some global objects may result in values that change over time, we block table imports // since access to some global objects may result in values that change over time, we block imports from non-readonly tables
for (const char* global : kSpecialGlobals) if (FFlag::LuauGenericSpecialGlobals)
{ {
AstName name = names.get(global); if (AstName name = names.get("_G"); name.value)
compiler.globals[name].writable = true;
if (name.value) if (options.mutableGlobals)
compiler.globals[name].special = true; for (const char** ptr = options.mutableGlobals; *ptr != NULL; ++ptr)
{
if (AstName name = names.get(*ptr); name.value)
compiler.globals[name].writable = true;
}
}
else
{
for (const char* global : kSpecialGlobals)
{
if (AstName name = names.get(global); name.value)
compiler.globals[name].writable = true;
}
} }
// this visitor traverses the AST to analyze mutability of locals/globals, filling Local::written and Global::written // this visitor traverses the AST to analyze mutability of locals/globals, filling Local::written and Global::written

View File

@ -49,7 +49,10 @@ OBJECTS=$(AST_OBJECTS) $(COMPILER_OBJECTS) $(ANALYSIS_OBJECTS) $(VM_OBJECTS) $(T
CXXFLAGS=-g -Wall -Werror CXXFLAGS=-g -Wall -Werror
LDFLAGS= LDFLAGS=
CXXFLAGS+=-Wno-unused # temporary, for older gcc versions # temporary, for older gcc versions as they treat var in `if (type var = val)` as unused
ifeq ($(findstring g++,$(shell $(CXX) --version)),g++)
CXXFLAGS+=-Wno-unused
endif
# configuration-specific flags # configuration-specific flags
ifeq ($(config),release) ifeq ($(config),release)

View File

@ -22,7 +22,7 @@ You can download the binaries from [a recent release](https://github.com/Roblox/
# Building # Building
To build Luau tools or tests yourself, you can use CMake on all platforms, or alternatively make (on Linux/macOS). For example: To build Luau tools or tests yourself, you can use CMake on all platforms:
```sh ```sh
mkdir cmake && cd cmake mkdir cmake && cd cmake
@ -31,6 +31,12 @@ cmake --build . --target Luau.Repl.CLI --config RelWithDebInfo
cmake --build . --target Luau.Analyze.CLI --config RelWithDebInfo cmake --build . --target Luau.Analyze.CLI --config RelWithDebInfo
``` ```
Alternatively, on Linux/macOS you can use make:
```sh
make config=release luau luau-analyze
```
To integrate Luau into your CMake application projects, at the minimum you'll need to depend on `Luau.Compiler` and `Luau.VM` projects. From there you need to create a new Luau state (using Lua 5.x API such as `lua_newstate`), compile source to bytecode and load it into the VM like this: To integrate Luau into your CMake application projects, at the minimum you'll need to depend on `Luau.Compiler` and `Luau.VM` projects. From there you need to create a new Luau state (using Lua 5.x API such as `lua_newstate`), compile source to bytecode and load it into the VM like this:
```cpp ```cpp

View File

@ -346,6 +346,8 @@ struct lua_Debug
* can only be changed when the VM is not running any code */ * can only be changed when the VM is not running any code */
struct lua_Callbacks struct lua_Callbacks
{ {
void* userdata; /* arbitrary userdata pointer that is never overwritten by Luau */
void (*interrupt)(lua_State* L, int gc); /* gets called at safepoints (loop back edges, call/ret, gc) if set */ void (*interrupt)(lua_State* L, int gc); /* gets called at safepoints (loop back edges, call/ret, gc) if set */
void (*panic)(lua_State* L, int errcode); /* gets called when an unprotected error is raised (if longjmp is used) */ void (*panic)(lua_State* L, int errcode); /* gets called when an unprotected error is raised (if longjmp is used) */

View File

@ -703,6 +703,7 @@ void lua_setreadonly(lua_State* L, int objindex, bool value)
const TValue* o = index2adr(L, objindex); const TValue* o = index2adr(L, objindex);
api_check(L, ttistable(o)); api_check(L, ttistable(o));
Table* t = hvalue(o); Table* t = hvalue(o);
api_check(L, t != hvalue(registry(L)));
t->readonly = value; t->readonly = value;
return; return;
} }

View File

@ -20,8 +20,9 @@
// If types of the arguments mismatch, luauF_* needs to return -1 and the execution will fall back to the usual call path // If types of the arguments mismatch, luauF_* needs to return -1 and the execution will fall back to the usual call path
// If luauF_* succeeds, it needs to return *all* requested arguments, filling results with nil as appropriate. // If luauF_* succeeds, it needs to return *all* requested arguments, filling results with nil as appropriate.
// On input, nparams refers to the actual number of arguments (0+), whereas nresults contains LUA_MULTRET for arbitrary returns or 0+ for a // On input, nparams refers to the actual number of arguments (0+), whereas nresults contains LUA_MULTRET for arbitrary returns or 0+ for a
// fixed-length return Because of this, and the fact that "extra" returned values will be ignored, implementations below typically check that nresults // fixed-length return
// is <= expected number, which covers the LUA_MULTRET case. // Because of this, and the fact that "extra" returned values will be ignored, implementations below typically check that nresults is <= expected
// number, which covers the LUA_MULTRET case.
static int luauF_assert(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams) static int luauF_assert(lua_State* L, StkId res, TValue* arg0, int nresults, StkId args, int nparams)
{ {

View File

@ -30,6 +30,7 @@ LUAU_FASTFLAGVARIABLE(LuauArrayBoundary, false)
#define MAXBITS 26 #define MAXBITS 26
#define MAXSIZE (1 << MAXBITS) #define MAXSIZE (1 << MAXBITS)
static_assert(offsetof(LuaNode, val) == 0, "Unexpected Node memory layout, pointer cast in gval2slot is incorrect");
// TKey is bitpacked for memory efficiency so we need to validate bit counts for worst case // TKey is bitpacked for memory efficiency so we need to validate bit counts for worst case
static_assert(TKey{{NULL}, 0, LUA_TDEADKEY, 0}.tt == LUA_TDEADKEY, "not enough bits for tt"); static_assert(TKey{{NULL}, 0, LUA_TDEADKEY, 0}.tt == LUA_TDEADKEY, "not enough bits for tt");
static_assert(TKey{{NULL}, 0, LUA_TNIL, MAXSIZE - 1}.next == MAXSIZE - 1, "not enough bits for next"); static_assert(TKey{{NULL}, 0, LUA_TNIL, MAXSIZE - 1}.next == MAXSIZE - 1, "not enough bits for next");

View File

@ -9,7 +9,6 @@
#define gval(n) (&(n)->val) #define gval(n) (&(n)->val)
#define gnext(n) ((n)->key.next) #define gnext(n) ((n)->key.next)
static_assert(offsetof(LuaNode, val) == 0, "Unexpected Node memory layout, pointer cast below is incorrect");
#define gval2slot(t, v) int(cast_to(LuaNode*, static_cast<const TValue*>(v)) - t->node) #define gval2slot(t, v) int(cast_to(LuaNode*, static_cast<const TValue*>(v)) - t->node)
LUAI_FUNC const TValue* luaH_getnum(Table* t, int key); LUAI_FUNC const TValue* luaH_getnum(Table* t, int key);

View File

@ -3,6 +3,8 @@ main:
url: /news url: /news
- title: Getting Started - title: Getting Started
url: /getting-started url: /getting-started
- title: GitHub
url: https://github.com/Roblox/luau
pages: pages:
- title: Getting Started - title: Getting Started
@ -25,3 +27,7 @@ pages:
url: /profile url: /profile
- title: Library - title: Library
url: /library url: /library
# Remove demo pages until solution is found
# - title: Demo
# url: /demo

50
docs/_includes/repl.html Normal file
View File

@ -0,0 +1,50 @@
<form>
<div>
<label>Script:</label>
<br>
<textarea rows="10" cols="70" id="script">print("Hello World!")</textarea>
<br><br>
<button onclick="clearInput(); return false;">
Clear Input
</button>
<button onclick="executeScript(); return false;">
Run
</button>
</div>
<br><br>
<div>
<label>Output:</label>
<br>
<textarea readonly rows="10" cols="70" id="output"></textarea>
<br><br>
<button onclick="clearOutput(); return false;">
Clear Output
</button>
</div>
</form>
<script>
function output(text) {
document.getElementById("output").value += "[" + new Date().toLocaleTimeString() + "] " + text.replace('stdin:', '') + "\n";
}
var Module = {
'print': function (msg) { output(msg) }
};
function clearInput() {
document.getElementById("script").value = "";
}
function clearOutput() {
document.getElementById("output").value = "";
}
function executeScript() {
var err = Module.ccall('executeScript', 'string', ['string'], [document.getElementById("script").value]);
if (err) {
output('Error:' + err.replace('stdin:', ''));
}
}
</script>
<script async src="assets/luau/luau.js"></script>

6
docs/_pages/demo.md Normal file
View File

@ -0,0 +1,6 @@
---
permalink: /demo
title: Demo
---
{% include repl.html %}

View File

@ -8,7 +8,7 @@ To get started with Luau you need to use `luau` command line binary to run your
## Creating a script ## Creating a script
To create your own testing script, create a new file with `.lua` as the extension: To create your own testing script, create a new file with `.luau` as the extension:
```lua ```lua
function ispositive(x) function ispositive(x)
@ -26,7 +26,7 @@ print(isfoo("bar"))
print(isfoo(1)) print(isfoo(1))
``` ```
You can now run the file using `luau test.lua` and analyze it using `luau-analyze test.lua`. You can now run the file using `luau test.luau` and analyze it using `luau-analyze test.luau`.
Note that there are no warnings about calling ``ispositive()`` with a string, or calling ``isfoo()`` a number. This is because the type checking uses non-strict mode by default, which is lenient in how it infers types used by the program. Note that there are no warnings about calling ``ispositive()`` with a string, or calling ``isfoo()`` a number. This is because the type checking uses non-strict mode by default, which is lenient in how it infers types used by the program.
@ -52,8 +52,8 @@ In this case, Luau will use the ``return x > 0`` statement to infer that ``ispos
Based on Luau's type inference, the analysis tool will now flag the incorrect call to ``ispositive()``: Based on Luau's type inference, the analysis tool will now flag the incorrect call to ``ispositive()``:
``` ```
$ luau-analyze test.lua $ luau-analyze test.luau
test.lua(7,18): TypeError: Type 'string' could not be converted into 'number' test.luau(7,18): TypeError: Type 'string' could not be converted into 'number'
``` ```
## Annotations ## Annotations
@ -92,9 +92,9 @@ result = ispositive(1)
Oops -- we're returning string values, but we forgot to update the function return type. Since we've told Luau that ``ispositive()`` returns a boolean (and that's how we're using it), the call site isn't flagged as an error. But because the annotation doesn't match our code, we get a warning in the function body itself: Oops -- we're returning string values, but we forgot to update the function return type. Since we've told Luau that ``ispositive()`` returns a boolean (and that's how we're using it), the call site isn't flagged as an error. But because the annotation doesn't match our code, we get a warning in the function body itself:
``` ```
$ luau-analyze test.lua $ luau-analyze test.luau
test.lua(5,9): TypeError: Type 'string' could not be converted into 'boolean' test.luau(5,9): TypeError: Type 'string' could not be converted into 'boolean'
test.lua(7,9): TypeError: Type 'string' could not be converted into 'boolean' test.luau(7,9): TypeError: Type 'string' could not be converted into 'boolean'
``` ```
The fix is simple; just change the annotation to declare the return type as a string: The fix is simple; just change the annotation to declare the return type as a string:
@ -117,8 +117,8 @@ result = ispositive(1)
Well, almost - since we declared ``result`` as a boolean, the call site is now flagged: Well, almost - since we declared ``result`` as a boolean, the call site is now flagged:
``` ```
$ luau-analyze test.lua $ luau-analyze test.luau
test.lua(12,10): TypeError: Type 'string' could not be converted into 'boolean' test.luau(12,10): TypeError: Type 'string' could not be converted into 'boolean'
``` ```
If we update the type of the local variable, everything is good. Note that we could also just let Luau infer the type of ``result`` by changing it to the single line version ``local result = ispositive(1)``. If we update the type of the local variable, everything is good. Note that we could also just let Luau infer the type of ``result`` by changing it to the single line version ``local result = ispositive(1)``.

View File

@ -56,4 +56,4 @@ This profiler doesn't track leaf C functions and instead attributes the time spe
slow, consider not just the work it does immediately but also the library functions it calls. slow, consider not just the work it does immediately but also the library functions it calls.
This profiler tracks time consumed by Luau thread stacks; when a thread calls another thread via `coroutine.resume`, the time spent is not attributed to the parent thread that's This profiler tracks time consumed by Luau thread stacks; when a thread calls another thread via `coroutine.resume`, the time spent is not attributed to the parent thread that's
waiting for resume results. This limitation will be removed in the future in the future. waiting for resume results. This limitation will be removed in the future.

View File

@ -46,16 +46,6 @@ This is using the VM feature that is not accessible from scripts, that prevents
By itself this would mean that code that runs in Luau can't use globals at all, since assigning globals would fail. While this is feasible, in Roblox we solve this by creating a new global table for each script, that uses `__index` to point to the builtin global table. This safely sandboxes the builtin globals while still allowing writing globals from each script. This also means that short of exposing special shared globals from the host, all scripts are isolated from each other. By itself this would mean that code that runs in Luau can't use globals at all, since assigning globals would fail. While this is feasible, in Roblox we solve this by creating a new global table for each script, that uses `__index` to point to the builtin global table. This safely sandboxes the builtin globals while still allowing writing globals from each script. This also means that short of exposing special shared globals from the host, all scripts are isolated from each other.
## Thread identity
Environment-level sandboxing is sufficient to implement separation between trusted code and untrusted code, assuming that `getfenv`/`setfenv` are either unavailable (removed from the globals), or that trusted code never interfaces with untrusted code (which prevents untrusted code from ever getting access to trusted functions). When running trusted code, it's possible to inject extra globals from the host into that global table, providing access to special APIs.
However, in some cases it's desirable to restrict access to functions that are exposed both to trusted and untrusted code. For example, both may have access to `game` global, but `game` may expose methods that should only work from trusted code.
To achieve this, each thread in Luau has a security identity, which can only be set by the host. Newly created threads inherit identities from the parent thread, and functions exposed from the host can validate the identity of the calling thread. This makes it possible to provide APIs to trusted code while limiting the access from untrusted code.
> Note: to achieve an even stronger guarantee of isolation between trusted and untrusted code, it's possible to run it in different Luau VMs, which is what Roblox does for extra safety.
## `__gc` ## `__gc`
Lua 5.1 exposes a `__gc` metamethod for userdata, which can be used on proxies (`newproxy`) to hook into garbage collector. Later versions of Lua extend this mechanism to work on tables. Lua 5.1 exposes a `__gc` metamethod for userdata, which can be used on proxies (`newproxy`) to hook into garbage collector. Later versions of Lua extend this mechanism to work on tables.

View File

@ -351,6 +351,30 @@ local onlyString: string = stringOrNumber -- ok
local onlyNumber: number = stringOrNumber -- not ok local onlyNumber: number = stringOrNumber -- not ok
``` ```
## Typecasts
Expressions may be typecast using `::`. Typecasting is useful for specifying the type of an expression when the automatically inferred type is too generic.
For example, consider the following table constructor where the intent is to store a table of names:
```lua
local myTable = {names = {}}
table.insert(myTable.names, 42) -- Inserting a number ought to cause a type error, but doesn't
```
In order to specify the type of the `names` table a typecast may be used:
```lua
local myTable = {names = {} :: {string}}
table.insert(myTable.names, 42) -- not ok, invalid 'number' to 'string' conversion
```
A typecast itself is also type checked to ensure the conversion is made to a subtype of the expression's type or `any`:
```lua
local numericValue = 1
local value = numericValue :: any -- ok, all expressions may be cast to 'any'
local flag = numericValue :: boolean -- not ok, invalid 'number' to 'boolean' conversion
```
## Roblox types ## Roblox types
Roblox supports a rich set of classes and data types, [documented here](https://developer.roblox.com/en-us/api-reference). All of them are readily available for the type checker to use by their name (e.g. `Part` or `RaycastResult`). Roblox supports a rich set of classes and data types, [documented here](https://developer.roblox.com/en-us/api-reference). All of them are readily available for the type checker to use by their name (e.g. `Part` or `RaycastResult`).
@ -397,3 +421,10 @@ return module
``` ```
There are some caveats here though. For instance, the require path must be resolvable statically, otherwise Luau cannot accurately type check it. There are some caveats here though. For instance, the require path must be resolvable statically, otherwise Luau cannot accurately type check it.
### Cyclic module dependencies
Cyclic module dependencies can cause problems for the type checker. In order to break a module dependency cycle a typecast of the module to `any` may be used:
```lua
local myModule = require(MyModule) :: any
```

View File

@ -0,0 +1,50 @@
# bit32.countlz/countrz
## Summary
Add bit32.countlz (count left zeroes) and bit32.countrz (count right zeroes) to accelerate bit scanning
## Motivation
All CPUs have instructions to determine the position of first/last set bit in an integer. These instructions have a variety of uses, the popular ones being:
- Fast implementation of integer logarithm (essentially allowing to compute `floor(log2(value))` quickly)
- Scanning set bits in an integer, which allows efficient traversal of compact representation of bitmaps
- Allocating bits out of a bitmap quickly
Today it's possible to approximate `countlz` using `floor` and `log` but this approximation is relatively slow; approximating `countrz` is difficult without iterating through each bit.
## Design
`bit32` library will gain two new functions, `countlz` and `countrz`:
```
function bit32.countlz(n: number): number
function bit32.countrz(n: number): number
```
`countlz` takes an integer number (converting the input number to a 32-bit unsigned integer as all other `bit32` functions do), and returns the number of consecutive left-most zero bits - that is, the number of most significant zero bits in a 32-bit number until the first 1. The result is in `[0, 32]` range.
For example, when the input number is `0`, it's `32`. When the input number is `2^k`, the result is `31-k`.
`countrz` takes an integer number (converting the input number to a 32-bit unsigned integer as all other `bit32` functions do), and returns the number of consecutive right-most zero bits - that is,
the number of least significant zero bits in a 32-bit number until the first 1. The result is in `[0, 32]` range.
For example, when the input number is `0`, it's `32`. When the input number is `2^k`, the result is `k`.
> Non-normative: a proof of concept implementation shows that a polyfill for `countlz` takes ~34 ns per loop iteration when computing `countlz` for an increasing number sequence, whereas
> a builtin implementation takes ~4 ns.
## Drawbacks
None known.
## Alternatives
These functions can be alternatively specified as "find the position of the most/least significant bit set" (e.g. "ffs"/"fls" for "find first set"/"find last set"). This formulation
can be more immediately useful since the bit position is usually more important than the number of bits. However, the bit position is undefined when the input number is zero,
returning a sentinel such as -1 seems non-idiomatic, and returning `nil` seems awkward for calling code. Counting functions don't have this problem.
An early version of this proposal suggested `clz`/`ctz` (leading/trailing) as names; however, using a full verb is more consistent with other operations like shift/rotate, and left/right may be easier to understand intuitively compared to leading/trailing. left/right are used by C++20.
Of the two functions, `countlz` is vastly more useful than `countrz`; we could implement just `countlz`, but having both is nice for symmetry.

View File

@ -13,6 +13,7 @@
LUAU_FASTFLAG(LuauPreloadClosures) LUAU_FASTFLAG(LuauPreloadClosures)
LUAU_FASTFLAG(LuauPreloadClosuresFenv) LUAU_FASTFLAG(LuauPreloadClosuresFenv)
LUAU_FASTFLAG(LuauPreloadClosuresUpval) LUAU_FASTFLAG(LuauPreloadClosuresUpval)
LUAU_FASTFLAG(LuauGenericSpecialGlobals)
using namespace Luau; using namespace Luau;
@ -1168,6 +1169,17 @@ RETURN R0 1
)"); )");
} }
TEST_CASE("ConstantFoldStringLen")
{
CHECK_EQ("\n" + compileFunction0("return #'string', #'', #'a', #('b')"), R"(
LOADN R0 6
LOADN R1 0
LOADN R2 1
LOADN R3 1
RETURN R0 4
)");
}
TEST_CASE("ConstantFoldCompare") TEST_CASE("ConstantFoldCompare")
{ {
// ordered comparisons // ordered comparisons
@ -3659,4 +3671,118 @@ RETURN R0 0
)"); )");
} }
TEST_CASE("LuauGenericSpecialGlobals")
{
const char* source = R"(
print()
Game.print()
Workspace.print()
_G.print()
game.print()
plugin.print()
script.print()
shared.print()
workspace.print()
)";
{
ScopedFastFlag genericSpecialGlobals{"LuauGenericSpecialGlobals", false};
// Check Roblox globals are here
CHECK_EQ("\n" + compileFunction0(source), R"(
GETIMPORT R0 1
CALL R0 0 0
GETIMPORT R1 3
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 5
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 7
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 9
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 11
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 13
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 15
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 17
GETTABLEKS R0 R1 K0
CALL R0 0 0
RETURN R0 0
)");
}
ScopedFastFlag genericSpecialGlobals{"LuauGenericSpecialGlobals", true};
// Check Roblox globals are no longer here
CHECK_EQ("\n" + compileFunction0(source), R"(
GETIMPORT R0 1
CALL R0 0 0
GETIMPORT R0 3
CALL R0 0 0
GETIMPORT R0 5
CALL R0 0 0
GETIMPORT R1 7
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R0 9
CALL R0 0 0
GETIMPORT R0 11
CALL R0 0 0
GETIMPORT R0 13
CALL R0 0 0
GETIMPORT R0 15
CALL R0 0 0
GETIMPORT R0 17
CALL R0 0 0
RETURN R0 0
)");
// Check we can add them back
Luau::BytecodeBuilder bcb;
bcb.setDumpFlags(Luau::BytecodeBuilder::Dump_Code);
Luau::CompileOptions options;
const char* mutableGlobals[] = {"Game", "Workspace", "game", "plugin", "script", "shared", "workspace", NULL};
options.mutableGlobals = &mutableGlobals[0];
Luau::compileOrThrow(bcb, source, options);
CHECK_EQ("\n" + bcb.dumpFunction(0), R"(
GETIMPORT R0 1
CALL R0 0 0
GETIMPORT R1 3
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 5
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 7
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 9
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 11
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 13
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 15
GETTABLEKS R0 R1 K0
CALL R0 0 0
GETIMPORT R1 17
GETTABLEKS R0 R1 K0
CALL R0 0 0
RETURN R0 0
)");
}
TEST_SUITE_END(); TEST_SUITE_END();