diff --git a/Analysis/include/Luau/TypeVar.h b/Analysis/include/Luau/TypeVar.h index fd2c2af..3f5e26d 100644 --- a/Analysis/include/Luau/TypeVar.h +++ b/Analysis/include/Luau/TypeVar.h @@ -1,6 +1,7 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include "Luau/DenseHash.h" #include "Luau/Predicate.h" #include "Luau/Unifiable.h" #include "Luau/Variant.h" @@ -499,6 +500,9 @@ bool maybeGeneric(const TypeId ty); // Checks if a type is of the form T1|...|Tn where one of the Ti is a singleton bool maybeSingleton(TypeId ty); +// Checks if the length operator can be applied on the value of type +bool hasLength(TypeId ty, DenseHashSet& seen, int* recursionCount); + struct SingletonTypes { const TypeId nilType; diff --git a/Analysis/src/Linter.cpp b/Analysis/src/Linter.cpp index 1a5b24f..905b70b 100644 --- a/Analysis/src/Linter.cpp +++ b/Analysis/src/Linter.cpp @@ -12,6 +12,8 @@ #include #include +LUAU_FASTFLAGVARIABLE(LuauLintTableCreateTable, false) + namespace Luau { @@ -2153,6 +2155,19 @@ private: "table.move uses index 0 but arrays are 1-based; did you mean 1 instead?"); } + if (FFlag::LuauLintTableCreateTable && func->index == "create" && node->args.size == 2) + { + // table.create(n, {...}) + if (args[1]->is()) + emitWarning(*context, LintWarning::Code_TableOperations, args[1]->location, + "table.create with a table literal will reuse the same object for all elements; consider using a for loop instead"); + + // table.create(n, {...} :: ?) + if (AstExprTypeAssertion* as = args[1]->as(); as && as->expr->is()) + emitWarning(*context, LintWarning::Code_TableOperations, as->expr->location, + "table.create with a table literal will reuse the same object for all elements; consider using a for loop instead"); + } + return true; } diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index bedcc02..e2d8a4f 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -33,6 +33,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false) LUAU_FASTFLAGVARIABLE(LuauRecursiveTypeParameterRestriction, false) LUAU_FASTFLAGVARIABLE(LuauIfElseBranchTypeUnion, false) LUAU_FASTFLAGVARIABLE(LuauIfElseExpectedType2, false) +LUAU_FASTFLAGVARIABLE(LuauLengthOnCompositeType, false) LUAU_FASTFLAGVARIABLE(LuauQuantifyInPlace2, false) LUAU_FASTFLAGVARIABLE(LuauSealExports, false) LUAU_FASTFLAGVARIABLE(LuauSingletonTypes, false) @@ -2066,17 +2067,27 @@ ExprResult TypeChecker::checkExpr(const ScopePtr& scope, const AstExprUn if (get(operandType)) return {errorRecoveryType(scope)}; - if (get(operandType)) - return {numberType}; // Not strictly correct: metatables permit overriding this - - if (auto p = get(operandType)) + if (FFlag::LuauLengthOnCompositeType) { - if (p->type == PrimitiveTypeVar::String) - return {numberType}; - } + DenseHashSet seen{nullptr}; - if (!getTableType(operandType)) - reportError(TypeError{expr.location, NotATable{operandType}}); + if (!hasLength(operandType, seen, &recursionCount)) + reportError(TypeError{expr.location, NotATable{operandType}}); + } + else + { + if (get(operandType)) + return {numberType}; // Not strictly correct: metatables permit overriding this + + if (auto p = get(operandType)) + { + if (p->type == PrimitiveTypeVar::String) + return {numberType}; + } + + if (!getTableType(operandType)) + reportError(TypeError{expr.location, NotATable{operandType}}); + } return {numberType}; diff --git a/Analysis/src/TypeVar.cpp b/Analysis/src/TypeVar.cpp index ac2b254..df5d76e 100644 --- a/Analysis/src/TypeVar.cpp +++ b/Analysis/src/TypeVar.cpp @@ -5,6 +5,7 @@ #include "Luau/Common.h" #include "Luau/DenseHash.h" #include "Luau/Error.h" +#include "Luau/RecursionCounter.h" #include "Luau/StringUtils.h" #include "Luau/ToString.h" #include "Luau/TypeInfer.h" @@ -19,6 +20,8 @@ LUAU_FASTINTVARIABLE(LuauTypeMaximumStringifierLength, 500) LUAU_FASTINTVARIABLE(LuauTableTypeMaximumStringifierLength, 0) +LUAU_FASTINT(LuauTypeInferRecursionLimit) +LUAU_FASTFLAG(LuauLengthOnCompositeType) LUAU_FASTFLAGVARIABLE(LuauMetatableAreEqualRecursion, false) LUAU_FASTFLAGVARIABLE(LuauRefactorTagging, false) LUAU_FASTFLAG(LuauErrorRecoveryType) @@ -326,6 +329,49 @@ bool maybeSingleton(TypeId ty) return false; } +bool hasLength(TypeId ty, DenseHashSet& seen, int* recursionCount) +{ + LUAU_ASSERT(FFlag::LuauLengthOnCompositeType); + + RecursionLimiter _rl(recursionCount, FInt::LuauTypeInferRecursionLimit); + + ty = follow(ty); + + if (seen.contains(ty)) + return true; + + if (isPrim(ty, PrimitiveTypeVar::String) || get(ty) || get(ty) || get(ty)) + return true; + + if (auto uty = get(ty)) + { + seen.insert(ty); + + for (TypeId part : uty->options) + { + if (!hasLength(part, seen, recursionCount)) + return false; + } + + return true; + } + + if (auto ity = get(ty)) + { + seen.insert(ty); + + for (TypeId part : ity->parts) + { + if (hasLength(part, seen, recursionCount)) + return true; + } + + return false; + } + + return false; +} + FunctionTypeVar::FunctionTypeVar(TypePackId argTypes, TypePackId retType, std::optional defn, bool hasSelf) : argTypes(argTypes) , retType(retType) diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index 6873c65..2bd9cf8 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -13,6 +13,7 @@ LUAU_FASTINT(LuauTypeInferRecursionLimit); LUAU_FASTINT(LuauTypeInferTypePackLoopLimit); +LUAU_FASTFLAGVARIABLE(LuauCommittingTxnLogFreeTpPromote, false) LUAU_FASTFLAG(LuauUseCommittingTxnLog) LUAU_FASTINTVARIABLE(LuauTypeInferIterationLimit, 2000); LUAU_FASTFLAGVARIABLE(LuauTableSubtypingVariance2, false); @@ -99,6 +100,11 @@ struct PromoteTypeLevels bool operator()(TypePackId tp, const FreeTypePack&) { + // Surprise, it's actually a BoundTypePack that hasn't been committed yet. + // Calling getMutable on this will trigger an assertion. + if (FFlag::LuauCommittingTxnLogFreeTpPromote && FFlag::LuauUseCommittingTxnLog && !log.is(tp)) + return true; + promote(tp, FFlag::LuauUseCommittingTxnLog ? log.getMutable(tp) : getMutable(tp)); return true; } diff --git a/Ast/include/Luau/Ast.h b/Ast/include/Luau/Ast.h index 573850a..ac5950c 100644 --- a/Ast/include/Luau/Ast.h +++ b/Ast/include/Luau/Ast.h @@ -1265,7 +1265,8 @@ struct hash size_t operator()(const Luau::AstName& value) const { // note: since operator== uses pointer identity, hashing function uses it as well - return value.value ? std::hash()(value.value) : 0; + // the hasher is the same as DenseHashPointer (DenseHash.h) + return (uintptr_t(value.value) >> 4) ^ (uintptr_t(value.value) >> 9); } }; diff --git a/Ast/include/Luau/DenseHash.h b/Ast/include/Luau/DenseHash.h index a7b2515..65939be 100644 --- a/Ast/include/Luau/DenseHash.h +++ b/Ast/include/Luau/DenseHash.h @@ -12,10 +12,6 @@ namespace Luau { -// Internal implementation of DenseHashSet and DenseHashMap -namespace detail -{ - struct DenseHashPointer { size_t operator()(const void* key) const @@ -24,6 +20,10 @@ struct DenseHashPointer } }; +// Internal implementation of DenseHashSet and DenseHashMap +namespace detail +{ + template using DenseHashDefault = std::conditional_t, DenseHashPointer, std::hash>; diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index 77787cb..3c607d2 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -14,6 +14,7 @@ LUAU_FASTFLAGVARIABLE(LuauFixAmbiguousErrorRecoveryInAssign, false) LUAU_FASTFLAGVARIABLE(LuauParseSingletonTypes, false) LUAU_FASTFLAGVARIABLE(LuauParseTypeAliasDefaults, false) LUAU_FASTFLAGVARIABLE(LuauParseRecoverTypePackEllipsis, false) +LUAU_FASTFLAGVARIABLE(LuauStartingBrokenComment, false) namespace Luau { @@ -174,10 +175,23 @@ ParseResult Parser::parse(const char* buffer, size_t bufferSize, AstNameTable& n const Lexeme::Type type = p.lexer.current().type; const Location loc = p.lexer.current().location; - p.lexer.next(); + if (FFlag::LuauStartingBrokenComment) + { + if (options.captureComments) + p.commentLocations.push_back(Comment{type, loc}); - if (options.captureComments) - p.commentLocations.push_back(Comment{type, loc}); + if (type == Lexeme::BrokenComment) + break; + + p.lexer.next(); + } + else + { + p.lexer.next(); + + if (options.captureComments) + p.commentLocations.push_back(Comment{type, loc}); + } } p.lexer.setSkipComments(true); diff --git a/CLI/Repl.cpp b/CLI/Repl.cpp index cf95893..ecf29fa 100644 --- a/CLI/Repl.cpp +++ b/CLI/Repl.cpp @@ -35,10 +35,15 @@ enum class CompileFormat Binary }; +struct GlobalOptions +{ + int optimizationLevel = 1; +} globalOptions; + static Luau::CompileOptions copts() { Luau::CompileOptions result = {}; - result.optimizationLevel = 1; + result.optimizationLevel = globalOptions.optimizationLevel; result.debugLevel = 1; result.coverageLevel = coverageActive() ? 2 : 0; @@ -232,13 +237,14 @@ static std::string runCode(lua_State* L, const std::string& source) static void completeIndexer(lua_State* L, const char* editBuffer, size_t start, std::vector& completions) { std::string_view lookup = editBuffer + start; + char lastSep = 0; for (;;) { - size_t dot = lookup.find('.'); - std::string_view prefix = lookup.substr(0, dot); + size_t sep = lookup.find_first_of(".:"); + std::string_view prefix = lookup.substr(0, sep); - if (dot == std::string_view::npos) + if (sep == std::string_view::npos) { // table, key lua_pushnil(L); @@ -249,11 +255,22 @@ static void completeIndexer(lua_State* L, const char* editBuffer, size_t start, { // table, key, value std::string_view key = lua_tostring(L, -2); + int valueType = lua_type(L, -1); - if (!key.empty() && Luau::startsWith(key, prefix)) - completions.push_back(editBuffer + std::string(key.substr(prefix.size()))); + // If the last separator was a ':' (i.e. a method call) then only functions should be completed. + bool requiredValueType = (lastSep != ':' || valueType == LUA_TFUNCTION); + + if (!key.empty() && requiredValueType && Luau::startsWith(key, prefix)) + { + std::string completion(editBuffer + std::string(key.substr(prefix.size()))); + if (valueType == LUA_TFUNCTION) + { + // Add an opening paren for function calls by default. + completion += "("; + } + completions.push_back(completion); + } } - lua_pop(L, 1); } @@ -266,10 +283,21 @@ static void completeIndexer(lua_State* L, const char* editBuffer, size_t start, lua_rawget(L, -2); lua_remove(L, -2); - if (!lua_istable(L, -1)) + if (lua_type(L, -1) == LUA_TSTRING) + { + // Replace the string object with the string class to perform further lookups of string functions + // Note: We retrieve the string class from _G to prevent issues if the user assigns to `string`. + lua_getglobal(L, "_G"); + lua_pushlstring(L, "string", 6); + lua_rawget(L, -2); + lua_remove(L, -2); + LUAU_ASSERT(lua_istable(L, -1)); + } + else if (!lua_istable(L, -1)) break; - lookup.remove_prefix(dot + 1); + lastSep = lookup[sep]; + lookup.remove_prefix(sep + 1); } } @@ -279,7 +307,7 @@ static void completeIndexer(lua_State* L, const char* editBuffer, size_t start, 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] == '_')) + while (start > 0 && (isalnum(editBuffer[start - 1]) || editBuffer[start - 1] == '.' || editBuffer[start - 1] == ':' || editBuffer[start - 1] == '_')) start--; // look the value up in current global table first @@ -319,15 +347,8 @@ struct LinenoiseScopedHistory std::string historyFilepath; }; -static void runRepl() +static void runReplImpl(lua_State* L) { - 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); }); @@ -368,7 +389,18 @@ static void runRepl() } } -static bool runFile(const char* name, lua_State* GL) +static void runRepl() +{ + std::unique_ptr globalState(luaL_newstate(), lua_close); + lua_State* L = globalState.get(); + + setupState(L); + 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 source = readFile(name); if (!source) @@ -419,6 +451,10 @@ static bool runFile(const char* name, lua_State* GL) fprintf(stderr, "%s", error.c_str()); } + if (repl) + { + runReplImpl(L); + } lua_pop(GL, 1); return status == 0; } @@ -457,7 +493,7 @@ static bool compileFile(const char* name, CompileFormat format) bcb.setDumpSource(*source); } - Luau::compileOrThrow(bcb, *source); + Luau::compileOrThrow(bcb, *source, copts()); switch (format) { @@ -495,9 +531,11 @@ static void displayHelp(const char* argv0) printf(" --compile[=format]: compile input files and output resulting formatted bytecode (binary or text)\n"); printf("\n"); printf("Available options:\n"); - printf(" -h, --help: Display this usage message.\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"); + printf(" -h, --help: Display this usage message.\n"); + printf(" -i, --interactive: Run an interactive REPL after executing the last script specified.\n"); + printf(" -O: use compiler optimization level (n=0-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"); } @@ -519,6 +557,7 @@ int main(int argc, char** argv) CompileFormat compileFormat{}; int profile = 0; bool coverage = false; + bool interactive = false; // Set the mode if the user has explicitly specified one. int argStart = 1; @@ -540,8 +579,8 @@ int main(int argc, char** argv) } else { - fprintf(stdout, "Error: Unrecognized value for '--compile' specified.\n"); - return -1; + fprintf(stderr, "Error: Unrecognized value for '--compile' specified.\n"); + return 1; } } @@ -552,6 +591,20 @@ int main(int argc, char** argv) 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 (strcmp(argv[i], "--profile") == 0) { profile = 10000; // default to 10 KHz @@ -575,7 +628,7 @@ int main(int argc, char** argv) } else if (argv[i][0] == '-') { - fprintf(stdout, "Error: Unrecognized option '%s'.\n\n", argv[i]); + fprintf(stderr, "Error: Unrecognized option '%s'.\n\n", argv[i]); displayHelp(argv[0]); return 1; } @@ -623,8 +676,11 @@ int main(int argc, char** argv) int failed = 0; - for (const std::string& path : files) - failed += !runFile(path.c_str(), L); + 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) { diff --git a/CMakeLists.txt b/CMakeLists.txt index bafc59e..77cf47e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,12 @@ target_compile_options(Luau.Ast PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Analysis PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.VM PRIVATE ${LUAU_OPTIONS}) +if (MSVC AND MSVC_VERSION GREATER_EQUAL 1924) + # disable partial redundancy elimination which regresses interpreter codegen substantially in VS2022: + # https://developercommunity.visualstudio.com/t/performance-regression-on-a-complex-interpreter-lo/1631863 + set_source_files_properties(VM/src/lvmexecute.cpp PROPERTIES COMPILE_FLAGS /d2ssa-pre-) +endif() + if(LUAU_BUILD_CLI) target_compile_options(Luau.Repl.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Analyze.CLI PRIVATE ${LUAU_OPTIONS}) diff --git a/Compiler/src/TableShape.cpp b/Compiler/src/TableShape.cpp index 7d99f22..9dc2f0a 100644 --- a/Compiler/src/TableShape.cpp +++ b/Compiler/src/TableShape.cpp @@ -1,11 +1,16 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "TableShape.h" +LUAU_FASTFLAGVARIABLE(LuauPredictTableSizeLoop, false) + namespace Luau { namespace Compile { +// conservative limit for the loop bound that establishes table array size +static const int kMaxLoopBound = 16; + static AstExprTable* getTableHint(AstExpr* expr) { // unadorned table literal @@ -27,7 +32,7 @@ struct ShapeVisitor : AstVisitor { size_t operator()(const std::pair& p) const { - return std::hash()(p.first) ^ std::hash()(p.second); + return DenseHashPointer()(p.first) ^ std::hash()(p.second); } }; @@ -36,10 +41,13 @@ struct ShapeVisitor : AstVisitor DenseHashMap tables; DenseHashSet, Hasher> fields; + DenseHashMap loops; // iterator => upper bound for 1..k + ShapeVisitor(DenseHashMap& shapes) : shapes(shapes) , tables(nullptr) , fields(std::pair()) + , loops(nullptr) { } @@ -63,16 +71,31 @@ struct ShapeVisitor : AstVisitor void assignField(AstExpr* expr, AstExpr* index) { AstExprLocal* lv = expr->as(); - AstExprConstantNumber* number = index->as(); + if (!lv) + return; - if (lv && number) + AstExprTable** table = tables.find(lv->local); + if (!table) + return; + + if (AstExprConstantNumber* number = index->as()) { - if (AstExprTable** table = tables.find(lv->local)) + TableShape& shape = shapes[*table]; + + if (number->value == double(shape.arraySize + 1)) + shape.arraySize += 1; + } + else if (AstExprLocal* iter = index->as()) + { + if (!FFlag::LuauPredictTableSizeLoop) + return; + + if (const unsigned int* bound = loops.find(iter->local)) { TableShape& shape = shapes[*table]; - if (number->value == double(shape.arraySize + 1)) - shape.arraySize += 1; + if (shape.arraySize == 0) + shape.arraySize = *bound; } } } @@ -117,6 +140,20 @@ struct ShapeVisitor : AstVisitor return false; } + + bool visit(AstStatFor* node) override + { + if (!FFlag::LuauPredictTableSizeLoop) + return true; + + AstExprConstantNumber* from = node->from->as(); + AstExprConstantNumber* to = node->to->as(); + + if (from && to && from->value == 1.0 && to->value >= 1.0 && to->value <= double(kMaxLoopBound) && !node->step) + loops[node->var] = unsigned(to->value); + + return true; + } }; void predictTableShapes(DenseHashMap& shapes, AstNode* root) diff --git a/VM/src/ldo.cpp b/VM/src/ldo.cpp index 3cce766..581506a 100644 --- a/VM/src/ldo.cpp +++ b/VM/src/ldo.cpp @@ -150,12 +150,11 @@ l_noret luaD_throw(lua_State* L, int errcode) static void correctstack(lua_State* L, TValue* oldstack) { - CallInfo* ci; - GCObject* up; L->top = (L->top - oldstack) + L->stack; - for (up = L->openupval; up != NULL; up = up->gch.next) - gco2uv(up)->v = (gco2uv(up)->v - oldstack) + L->stack; - for (ci = L->base_ci; ci <= L->ci; ci++) + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + for (UpVal* up = L->openupval; up != NULL; up = (UpVal*)up->next) + up->v = (up->v - oldstack) + L->stack; + for (CallInfo* ci = L->base_ci; ci <= L->ci; ci++) { ci->top = (ci->top - oldstack) + L->stack; ci->base = (ci->base - oldstack) + L->stack; diff --git a/VM/src/lfunc.cpp b/VM/src/lfunc.cpp index 4178eda..6088f71 100644 --- a/VM/src/lfunc.cpp +++ b/VM/src/lfunc.cpp @@ -7,10 +7,11 @@ #include "lgc.h" LUAU_FASTFLAGVARIABLE(LuauNoDirectUpvalRemoval, false) +LUAU_FASTFLAG(LuauGcPagedSweep) Proto* luaF_newproto(lua_State* L) { - Proto* f = luaM_new(L, Proto, sizeof(Proto), L->activememcat); + Proto* f = luaM_newgco(L, Proto, sizeof(Proto), L->activememcat); luaC_link(L, f, LUA_TPROTO); f->k = NULL; f->sizek = 0; @@ -38,7 +39,7 @@ Proto* luaF_newproto(lua_State* L) Closure* luaF_newLclosure(lua_State* L, int nelems, Table* e, Proto* p) { - Closure* c = luaM_new(L, Closure, sizeLclosure(nelems), L->activememcat); + Closure* c = luaM_newgco(L, Closure, sizeLclosure(nelems), L->activememcat); luaC_link(L, c, LUA_TFUNCTION); c->isC = 0; c->env = e; @@ -53,7 +54,7 @@ Closure* luaF_newLclosure(lua_State* L, int nelems, Table* e, Proto* p) Closure* luaF_newCclosure(lua_State* L, int nelems, Table* e) { - Closure* c = luaM_new(L, Closure, sizeCclosure(nelems), L->activememcat); + Closure* c = luaM_newgco(L, Closure, sizeCclosure(nelems), L->activememcat); luaC_link(L, c, LUA_TFUNCTION); c->isC = 1; c->env = e; @@ -69,10 +70,9 @@ Closure* luaF_newCclosure(lua_State* L, int nelems, Table* e) UpVal* luaF_findupval(lua_State* L, StkId level) { global_State* g = L->global; - GCObject** pp = &L->openupval; + UpVal** pp = &L->openupval; UpVal* p; - UpVal* uv; - while (*pp != NULL && (p = gco2uv(*pp))->v >= level) + while (*pp != NULL && (p = *pp)->v >= level) { LUAU_ASSERT(p->v != &p->u.value); if (p->v == level) @@ -81,53 +81,95 @@ UpVal* luaF_findupval(lua_State* L, StkId level) changewhite(obj2gco(p)); /* resurrect it */ return p; } - pp = &p->next; + + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + pp = (UpVal**)&p->next; } - uv = luaM_new(L, UpVal, sizeof(UpVal), L->activememcat); /* not found: create a new one */ + + UpVal* uv = luaM_newgco(L, UpVal, sizeof(UpVal), L->activememcat); /* not found: create a new one */ uv->tt = LUA_TUPVAL; uv->marked = luaC_white(g); uv->memcat = L->activememcat; uv->v = level; /* current value lives in the stack */ - uv->next = *pp; /* chain it in the proper position */ - *pp = obj2gco(uv); - uv->u.l.prev = &g->uvhead; /* double link it in `uvhead' list */ + + // chain the upvalue in the threads open upvalue list at the proper position + UpVal* next = *pp; + + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + uv->next = (GCObject*)next; + + if (FFlag::LuauGcPagedSweep) + { + uv->u.l.threadprev = pp; + if (next) + { + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + next->u.l.threadprev = (UpVal**)&uv->next; + } + } + + *pp = uv; + + // double link the upvalue in the global open upvalue list + uv->u.l.prev = &g->uvhead; uv->u.l.next = g->uvhead.u.l.next; uv->u.l.next->u.l.prev = uv; g->uvhead.u.l.next = uv; LUAU_ASSERT(uv->u.l.next->u.l.prev == uv && uv->u.l.prev->u.l.next == uv); return uv; } - -static void unlinkupval(UpVal* uv) +void luaF_unlinkupval(UpVal* uv) { + // unlink upvalue from the global open upvalue list LUAU_ASSERT(uv->u.l.next->u.l.prev == uv && uv->u.l.prev->u.l.next == uv); - uv->u.l.next->u.l.prev = uv->u.l.prev; /* remove from `uvhead' list */ + uv->u.l.next->u.l.prev = uv->u.l.prev; uv->u.l.prev->u.l.next = uv->u.l.next; + + if (FFlag::LuauGcPagedSweep) + { + // unlink upvalue from the thread open upvalue list + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and this and the following cast will not be required + *uv->u.l.threadprev = (UpVal*)uv->next; + + if (UpVal* next = (UpVal*)uv->next) + next->u.l.threadprev = uv->u.l.threadprev; + } } -void luaF_freeupval(lua_State* L, UpVal* uv) +void luaF_freeupval(lua_State* L, UpVal* uv, lua_Page* page) { if (uv->v != &uv->u.value) /* is it open? */ - unlinkupval(uv); /* remove from open list */ - luaM_free(L, uv, sizeof(UpVal), uv->memcat); /* free upvalue */ + luaF_unlinkupval(uv); /* remove from open list */ + luaM_freegco(L, uv, sizeof(UpVal), uv->memcat, page); /* free upvalue */ } void luaF_close(lua_State* L, StkId level) { - UpVal* uv; global_State* g = L->global; // TODO: remove with FFlagLuauNoDirectUpvalRemoval - while (L->openupval != NULL && (uv = gco2uv(L->openupval))->v >= level) + UpVal* uv; + while (L->openupval != NULL && (uv = L->openupval)->v >= level) { GCObject* o = obj2gco(uv); LUAU_ASSERT(!isblack(o) && uv->v != &uv->u.value); - L->openupval = uv->next; /* remove from `open' list */ - if (!FFlag::LuauNoDirectUpvalRemoval && isdead(g, o)) + + if (!FFlag::LuauGcPagedSweep) + L->openupval = (UpVal*)uv->next; /* remove from `open' list */ + + if (FFlag::LuauGcPagedSweep && isdead(g, o)) { - luaF_freeupval(L, uv); /* free upvalue */ + // by removing the upvalue from global/thread open upvalue lists, L->openupval will be pointing to the next upvalue + luaF_unlinkupval(uv); + // close the upvalue without copying the dead data so that luaF_freeupval will not unlink again + uv->v = &uv->u.value; + } + else if (!FFlag::LuauNoDirectUpvalRemoval && isdead(g, o)) + { + luaF_freeupval(L, uv, NULL); /* free upvalue */ } else { - unlinkupval(uv); + // by removing the upvalue from global/thread open upvalue lists, L->openupval will be pointing to the next upvalue + luaF_unlinkupval(uv); setobj(L, &uv->u.value, uv->v); uv->v = &uv->u.value; /* now current value lives here */ luaC_linkupval(L, uv); /* link upvalue into `gcroot' list */ @@ -135,7 +177,7 @@ void luaF_close(lua_State* L, StkId level) } } -void luaF_freeproto(lua_State* L, Proto* f) +void luaF_freeproto(lua_State* L, Proto* f, lua_Page* page) { luaM_freearray(L, f->code, f->sizecode, Instruction, f->memcat); luaM_freearray(L, f->p, f->sizep, Proto*, f->memcat); @@ -146,13 +188,13 @@ void luaF_freeproto(lua_State* L, Proto* f) luaM_freearray(L, f->upvalues, f->sizeupvalues, TString*, f->memcat); if (f->debuginsn) luaM_freearray(L, f->debuginsn, f->sizecode, uint8_t, f->memcat); - luaM_free(L, f, sizeof(Proto), f->memcat); + luaM_freegco(L, f, sizeof(Proto), f->memcat, page); } -void luaF_freeclosure(lua_State* L, Closure* c) +void luaF_freeclosure(lua_State* L, Closure* c, lua_Page* page) { int size = c->isC ? sizeCclosure(c->nupvalues) : sizeLclosure(c->nupvalues); - luaM_free(L, c, size, c->memcat); + luaM_freegco(L, c, size, c->memcat, page); } const LocVar* luaF_getlocal(const Proto* f, int local_number, int pc) diff --git a/VM/src/lfunc.h b/VM/src/lfunc.h index 4be2366..8047ceb 100644 --- a/VM/src/lfunc.h +++ b/VM/src/lfunc.h @@ -12,7 +12,8 @@ LUAI_FUNC Closure* luaF_newLclosure(lua_State* L, int nelems, Table* e, Proto* p LUAI_FUNC Closure* luaF_newCclosure(lua_State* L, int nelems, Table* e); LUAI_FUNC UpVal* luaF_findupval(lua_State* L, StkId level); LUAI_FUNC void luaF_close(lua_State* L, StkId level); -LUAI_FUNC void luaF_freeproto(lua_State* L, Proto* f); -LUAI_FUNC void luaF_freeclosure(lua_State* L, Closure* c); -LUAI_FUNC void luaF_freeupval(lua_State* L, UpVal* uv); +LUAI_FUNC void luaF_freeproto(lua_State* L, Proto* f, struct lua_Page* page); +LUAI_FUNC void luaF_freeclosure(lua_State* L, Closure* c, struct lua_Page* page); +void luaF_unlinkupval(UpVal* uv); +LUAI_FUNC void luaF_freeupval(lua_State* L, UpVal* uv, struct lua_Page* page); LUAI_FUNC const LocVar* luaF_getlocal(const Proto* func, int local_number, int pc); diff --git a/VM/src/lgc.cpp b/VM/src/lgc.cpp index 76ef7a0..9a8cb07 100644 --- a/VM/src/lgc.cpp +++ b/VM/src/lgc.cpp @@ -8,12 +8,16 @@ #include "lfunc.h" #include "lstring.h" #include "ldo.h" +#include "lmem.h" #include "ludata.h" #include +LUAU_FASTFLAGVARIABLE(LuauGcPagedSweep, false) + #define GC_SWEEPMAX 40 #define GC_SWEEPCOST 10 +#define GC_SWEEPPAGESTEPCOST 4 #define GC_INTERRUPT(state) \ { \ @@ -457,31 +461,31 @@ static void shrinkstack(lua_State* L) condhardstacktests(luaD_reallocstack(L, s_used)); } -static void freeobj(lua_State* L, GCObject* o) +static void freeobj(lua_State* L, GCObject* o, lua_Page* page) { switch (o->gch.tt) { case LUA_TPROTO: - luaF_freeproto(L, gco2p(o)); + luaF_freeproto(L, gco2p(o), page); break; case LUA_TFUNCTION: - luaF_freeclosure(L, gco2cl(o)); + luaF_freeclosure(L, gco2cl(o), page); break; case LUA_TUPVAL: - luaF_freeupval(L, gco2uv(o)); + luaF_freeupval(L, gco2uv(o), page); break; case LUA_TTABLE: - luaH_free(L, gco2h(o)); + luaH_free(L, gco2h(o), page); break; case LUA_TTHREAD: LUAU_ASSERT(gco2th(o) != L && gco2th(o) != L->global->mainthread); - luaE_freethread(L, gco2th(o)); + luaE_freethread(L, gco2th(o), page); break; case LUA_TSTRING: - luaS_free(L, gco2ts(o)); + luaS_free(L, gco2ts(o), page); break; case LUA_TUSERDATA: - luaU_freeudata(L, gco2u(o)); + luaU_freeudata(L, gco2u(o), page); break; default: LUAU_ASSERT(0); @@ -492,6 +496,8 @@ static void freeobj(lua_State* L, GCObject* o) static GCObject** sweeplist(lua_State* L, GCObject** p, size_t count, size_t* traversedcount) { + LUAU_ASSERT(!FFlag::LuauGcPagedSweep); + GCObject* curr; global_State* g = L->global; int deadmask = otherwhite(g); @@ -502,7 +508,7 @@ static GCObject** sweeplist(lua_State* L, GCObject** p, size_t count, size_t* tr int alive = (curr->gch.marked ^ WHITEBITS) & deadmask; if (curr->gch.tt == LUA_TTHREAD) { - sweepwholelist(L, &gco2th(curr)->openupval, traversedcount); /* sweep open upvalues */ + sweepwholelist(L, (GCObject**)&gco2th(curr)->openupval, traversedcount); /* sweep open upvalues */ lua_State* th = gco2th(curr); @@ -524,7 +530,7 @@ static GCObject** sweeplist(lua_State* L, GCObject** p, size_t count, size_t* tr *p = curr->gch.next; if (curr == g->rootgc) /* is the first element of the list? */ g->rootgc = curr->gch.next; /* adjust first */ - freeobj(L, curr); + freeobj(L, curr, NULL); } } @@ -537,14 +543,16 @@ static GCObject** sweeplist(lua_State* L, GCObject** p, size_t count, size_t* tr static void deletelist(lua_State* L, GCObject** p, GCObject* limit) { + LUAU_ASSERT(!FFlag::LuauGcPagedSweep); + GCObject* curr; while ((curr = *p) != limit) { if (curr->gch.tt == LUA_TTHREAD) /* delete open upvalues of each thread */ - deletelist(L, &gco2th(curr)->openupval, NULL); + deletelist(L, (GCObject**)&gco2th(curr)->openupval, NULL); *p = curr->gch.next; - freeobj(L, curr); + freeobj(L, curr, NULL); } } @@ -567,23 +575,62 @@ static void shrinkbuffersfull(lua_State* L) luaS_resize(L, hashsize); /* table is too big */ } +static bool deletegco(void* context, lua_Page* page, GCObject* gco) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + // we are in the process of deleting everything + // threads with open upvalues will attempt to close them all on removal + // but those upvalues might point to stack values that were already deleted + if (gco->gch.tt == LUA_TTHREAD) + { + lua_State* th = gco2th(gco); + + while (UpVal* uv = th->openupval) + { + luaF_unlinkupval(uv); + // close the upvalue without copying the dead data so that luaF_freeupval will not unlink again + uv->v = &uv->u.value; + } + } + + lua_State* L = (lua_State*)context; + freeobj(L, gco, page); + return true; +} + void luaC_freeall(lua_State* L) { global_State* g = L->global; LUAU_ASSERT(L == g->mainthread); - LUAU_ASSERT(L->next == NULL); /* mainthread is at the end of rootgc list */ - deletelist(L, &g->rootgc, obj2gco(L)); + if (FFlag::LuauGcPagedSweep) + { + luaM_visitgco(L, L, deletegco); - for (int i = 0; i < g->strt.size; i++) /* free all string lists */ - deletelist(L, &g->strt.hash[i], NULL); + for (int i = 0; i < g->strt.size; i++) /* free all string lists */ + LUAU_ASSERT(g->strt.hash[i] == NULL); - LUAU_ASSERT(L->global->strt.nuse == 0); - deletelist(L, &g->strbufgc, NULL); - // unfortunately, when string objects are freed, the string table use count is decremented - // even when the string is a buffer that wasn't placed into the table - L->global->strt.nuse = 0; + LUAU_ASSERT(L->global->strt.nuse == 0); + LUAU_ASSERT(g->strbufgc == NULL); + } + else + { + LUAU_ASSERT(L->next == NULL); /* mainthread is at the end of rootgc list */ + + deletelist(L, &g->rootgc, obj2gco(L)); + + for (int i = 0; i < g->strt.size; i++) /* free all string lists */ + deletelist(L, (GCObject**)&g->strt.hash[i], NULL); + + LUAU_ASSERT(L->global->strt.nuse == 0); + deletelist(L, (GCObject**)&g->strbufgc, NULL); + + // unfortunately, when string objects are freed, the string table use count is decremented + // even when the string is a buffer that wasn't placed into the table + L->global->strt.nuse = 0; + } } static void markmt(global_State* g) @@ -648,12 +695,88 @@ static size_t atomic(lua_State* L) /* flip current white */ g->currentwhite = cast_byte(otherwhite(g)); g->sweepstrgc = 0; - g->sweepgc = &g->rootgc; - g->gcstate = GCSsweepstring; + + if (FFlag::LuauGcPagedSweep) + { + g->sweepgcopage = g->allgcopages; + g->gcstate = GCSsweep; + } + else + { + g->sweepgc = &g->rootgc; + g->gcstate = GCSsweepstring; + } return work; } +static bool sweepgco(lua_State* L, lua_Page* page, GCObject* gco) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + global_State* g = L->global; + + int deadmask = otherwhite(g); + LUAU_ASSERT(testbit(deadmask, FIXEDBIT)); // make sure we never sweep fixed objects + + int alive = (gco->gch.marked ^ WHITEBITS) & deadmask; + + g->gcstats.currcycle.sweepitems++; + + if (gco->gch.tt == LUA_TTHREAD) + { + lua_State* th = gco2th(gco); + + if (alive) + { + resetbit(th->stackstate, THREAD_SLEEPINGBIT); + shrinkstack(th); + } + } + + if (alive) + { + LUAU_ASSERT(!isdead(g, gco)); + makewhite(g, gco); // make it white (for next cycle) + return false; + } + + LUAU_ASSERT(isdead(g, gco)); + freeobj(L, gco, page); + return true; +} + +// a version of generic luaM_visitpage specialized for the main sweep stage +static int sweepgcopage(lua_State* L, lua_Page* page) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + char* start; + char* end; + int busyBlocks; + int blockSize; + luaM_getpagewalkinfo(page, &start, &end, &busyBlocks, &blockSize); + + for (char* pos = start; pos != end; pos += blockSize) + { + GCObject* gco = (GCObject*)pos; + + // skip memory blocks that are already freed + if (gco->gch.tt == LUA_TNIL) + continue; + + // when true is returned it means that the element was deleted + if (sweepgco(L, page, gco)) + { + // if the last block was removed, page would be removed as well + if (--busyBlocks == 0) + return int(pos - start) / blockSize + 1; + } + } + + return int(end - start) / blockSize; +} + static size_t gcstep(lua_State* L, size_t limit) { size_t cost = 0; @@ -706,15 +829,21 @@ static size_t gcstep(lua_State* L, size_t limit) g->gcstats.currcycle.atomicstarttotalsizebytes = g->totalbytes; cost = atomic(L); /* finish mark phase */ - LUAU_ASSERT(g->gcstate == GCSsweepstring); + + if (FFlag::LuauGcPagedSweep) + LUAU_ASSERT(g->gcstate == GCSsweep); + else + LUAU_ASSERT(g->gcstate == GCSsweepstring); break; } case GCSsweepstring: { + LUAU_ASSERT(!FFlag::LuauGcPagedSweep); + while (g->sweepstrgc < g->strt.size && cost < limit) { size_t traversedcount = 0; - sweepwholelist(L, &g->strt.hash[g->sweepstrgc++], &traversedcount); + sweepwholelist(L, (GCObject**)&g->strt.hash[g->sweepstrgc++], &traversedcount); g->gcstats.currcycle.sweepitems += traversedcount; cost += GC_SWEEPCOST; @@ -727,7 +856,7 @@ static size_t gcstep(lua_State* L, size_t limit) uint32_t nuse = L->global->strt.nuse; size_t traversedcount = 0; - sweepwholelist(L, &g->strbufgc, &traversedcount); + sweepwholelist(L, (GCObject**)&g->strbufgc, &traversedcount); L->global->strt.nuse = nuse; @@ -738,19 +867,44 @@ static size_t gcstep(lua_State* L, size_t limit) } case GCSsweep: { - while (*g->sweepgc && cost < limit) + if (FFlag::LuauGcPagedSweep) { - size_t traversedcount = 0; - g->sweepgc = sweeplist(L, g->sweepgc, GC_SWEEPMAX, &traversedcount); + while (g->sweepgcopage && cost < limit) + { + lua_Page* next = luaM_getnextgcopage(g->sweepgcopage); // page sweep might destroy the page - g->gcstats.currcycle.sweepitems += traversedcount; - cost += GC_SWEEPMAX * GC_SWEEPCOST; + int steps = sweepgcopage(L, g->sweepgcopage); + + g->sweepgcopage = next; + cost += steps * GC_SWEEPPAGESTEPCOST; + } + + // nothing more to sweep? + if (g->sweepgcopage == NULL) + { + // don't forget to visit main thread + sweepgco(L, NULL, obj2gco(g->mainthread)); + + shrinkbuffers(L); + g->gcstate = GCSpause; /* end collection */ + } } + else + { + while (*g->sweepgc && cost < limit) + { + size_t traversedcount = 0; + g->sweepgc = sweeplist(L, g->sweepgc, GC_SWEEPMAX, &traversedcount); - if (*g->sweepgc == NULL) - { /* nothing more to sweep? */ - shrinkbuffers(L); - g->gcstate = GCSpause; /* end collection */ + g->gcstats.currcycle.sweepitems += traversedcount; + cost += GC_SWEEPMAX * GC_SWEEPCOST; + } + + if (*g->sweepgc == NULL) + { /* nothing more to sweep? */ + shrinkbuffers(L); + g->gcstate = GCSpause; /* end collection */ + } } break; } @@ -877,12 +1031,19 @@ void luaC_fullgc(lua_State* L) { /* reset sweep marks to sweep all elements (returning them to white) */ g->sweepstrgc = 0; - g->sweepgc = &g->rootgc; + if (FFlag::LuauGcPagedSweep) + g->sweepgcopage = g->allgcopages; + else + g->sweepgc = &g->rootgc; /* reset other collector lists */ g->gray = NULL; g->grayagain = NULL; g->weak = NULL; - g->gcstate = GCSsweepstring; + + if (FFlag::LuauGcPagedSweep) + g->gcstate = GCSsweep; + else + g->gcstate = GCSsweepstring; } LUAU_ASSERT(g->gcstate == GCSsweepstring || g->gcstate == GCSsweep); /* finish any pending sweep phase */ @@ -979,8 +1140,11 @@ void luaC_barrierback(lua_State* L, Table* t) void luaC_linkobj(lua_State* L, GCObject* o, uint8_t tt) { global_State* g = L->global; - o->gch.next = g->rootgc; - g->rootgc = o; + if (!FFlag::LuauGcPagedSweep) + { + o->gch.next = g->rootgc; + g->rootgc = o; + } o->gch.marked = luaC_white(g); o->gch.tt = tt; o->gch.memcat = L->activememcat; @@ -990,8 +1154,13 @@ void luaC_linkupval(lua_State* L, UpVal* uv) { global_State* g = L->global; GCObject* o = obj2gco(uv); - o->gch.next = g->rootgc; /* link upvalue into `rootgc' list */ - g->rootgc = o; + + if (!FFlag::LuauGcPagedSweep) + { + o->gch.next = g->rootgc; /* link upvalue into `rootgc' list */ + g->rootgc = o; + } + if (isgray(o)) { if (keepinvariant(g)) diff --git a/VM/src/lgc.h b/VM/src/lgc.h index f434e50..4455fec 100644 --- a/VM/src/lgc.h +++ b/VM/src/lgc.h @@ -13,6 +13,7 @@ #define GCSpropagate 1 #define GCSpropagateagain 2 #define GCSatomic 3 +// TODO: remove with FFlagLuauGcPagedSweep #define GCSsweepstring 4 #define GCSsweep 5 diff --git a/VM/src/lgcdebug.cpp b/VM/src/lgcdebug.cpp index c66de9c..906fb0d 100644 --- a/VM/src/lgcdebug.cpp +++ b/VM/src/lgcdebug.cpp @@ -2,16 +2,19 @@ // This code is based on Lua 5.x implementation licensed under MIT License; see lua_LICENSE.txt for details #include "lgc.h" +#include "lfunc.h" +#include "lmem.h" #include "lobject.h" #include "lstate.h" -#include "ltable.h" -#include "lfunc.h" #include "lstring.h" +#include "ltable.h" #include "ludata.h" #include #include +LUAU_FASTFLAG(LuauGcPagedSweep) + static void validateobjref(global_State* g, GCObject* f, GCObject* t) { LUAU_ASSERT(!isdead(g, t)); @@ -101,10 +104,11 @@ static void validatestack(global_State* g, lua_State* l) if (l->namecall) validateobjref(g, obj2gco(l), obj2gco(l->namecall)); - for (GCObject* uv = l->openupval; uv; uv = uv->gch.next) + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + for (UpVal* uv = l->openupval; uv; uv = (UpVal*)uv->next) { - LUAU_ASSERT(uv->gch.tt == LUA_TUPVAL); - LUAU_ASSERT(gco2uv(uv)->v != &gco2uv(uv)->u.value); + LUAU_ASSERT(uv->tt == LUA_TUPVAL); + LUAU_ASSERT(uv->v != &uv->u.value); } } @@ -178,6 +182,8 @@ static void validateobj(global_State* g, GCObject* o) static void validatelist(global_State* g, GCObject* o) { + LUAU_ASSERT(!FFlag::LuauGcPagedSweep); + while (o) { validateobj(g, o); @@ -216,6 +222,17 @@ static void validategraylist(global_State* g, GCObject* o) } } +static bool validategco(void* context, lua_Page* page, GCObject* gco) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + lua_State* L = (lua_State*)context; + global_State* g = L->global; + + validateobj(g, gco); + return false; +} + void luaC_validate(lua_State* L) { global_State* g = L->global; @@ -231,11 +248,18 @@ void luaC_validate(lua_State* L) validategraylist(g, g->gray); validategraylist(g, g->grayagain); - for (int i = 0; i < g->strt.size; ++i) - validatelist(g, g->strt.hash[i]); + if (FFlag::LuauGcPagedSweep) + { + luaM_visitgco(L, L, validategco); + } + else + { + for (int i = 0; i < g->strt.size; ++i) + validatelist(g, (GCObject*)(g->strt.hash[i])); - validatelist(g, g->rootgc); - validatelist(g, g->strbufgc); + validatelist(g, g->rootgc); + validatelist(g, (GCObject*)(g->strbufgc)); + } for (UpVal* uv = g->uvhead.u.l.next; uv != &g->uvhead; uv = uv->u.l.next) { @@ -499,6 +523,8 @@ static void dumpobj(FILE* f, GCObject* o) static void dumplist(FILE* f, GCObject* o) { + LUAU_ASSERT(!FFlag::LuauGcPagedSweep); + while (o) { dumpref(f, o); @@ -509,22 +535,45 @@ static void dumplist(FILE* f, GCObject* o) // thread has additional list containing collectable objects that are not present in rootgc if (o->gch.tt == LUA_TTHREAD) - dumplist(f, gco2th(o)->openupval); + dumplist(f, (GCObject*)gco2th(o)->openupval); o = o->gch.next; } } +static bool dumpgco(void* context, lua_Page* page, GCObject* gco) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + FILE* f = (FILE*)context; + + dumpref(f, gco); + fputc(':', f); + dumpobj(f, gco); + fputc(',', f); + fputc('\n', f); + + return false; +} + void luaC_dump(lua_State* L, void* file, const char* (*categoryName)(lua_State* L, uint8_t memcat)) { global_State* g = L->global; FILE* f = static_cast(file); fprintf(f, "{\"objects\":{\n"); - dumplist(f, g->rootgc); - dumplist(f, g->strbufgc); - for (int i = 0; i < g->strt.size; ++i) - dumplist(f, g->strt.hash[i]); + + if (FFlag::LuauGcPagedSweep) + { + luaM_visitgco(L, f, dumpgco); + } + else + { + dumplist(f, g->rootgc); + dumplist(f, (GCObject*)(g->strbufgc)); + for (int i = 0; i < g->strt.size; ++i) + dumplist(f, (GCObject*)(g->strt.hash[i])); + } fprintf(f, "\"0\":{\"type\":\"userdata\",\"cat\":0,\"size\":0}\n"); // to avoid issues with trailing , fprintf(f, "},\"roots\":{\n"); diff --git a/VM/src/lmem.cpp b/VM/src/lmem.cpp index 9f9d4a9..e1dbce5 100644 --- a/VM/src/lmem.cpp +++ b/VM/src/lmem.cpp @@ -8,6 +8,8 @@ #include +LUAU_FASTFLAG(LuauGcPagedSweep) + #ifndef __has_feature #define __has_feature(x) 0 #endif @@ -42,13 +44,21 @@ static_assert(sizeof(LuaNode) == ABISWITCH(32, 32, 32), "size mismatch for table #endif static_assert(offsetof(TString, data) == ABISWITCH(24, 20, 20), "size mismatch for string header"); +// TODO (FFlagLuauGcPagedSweep): this will become ABISWITCH(16, 16, 16) static_assert(offsetof(Udata, data) == ABISWITCH(24, 16, 16), "size mismatch for userdata header"); +// TODO (FFlagLuauGcPagedSweep): this will become ABISWITCH(48, 32, 32) static_assert(sizeof(Table) == ABISWITCH(56, 36, 36), "size mismatch for table header"); +// TODO (FFlagLuauGcPagedSweep): new code with old 'next' pointer requires that GCObject start at the same point as TString/UpVal +static_assert(offsetof(GCObject, uv) == 0, "UpVal data must be located at the start of the GCObject"); +static_assert(offsetof(GCObject, ts) == 0, "TString data must be located at the start of the GCObject"); + const size_t kSizeClasses = LUA_SIZECLASSES; const size_t kMaxSmallSize = 512; const size_t kPageSize = 16 * 1024 - 24; // slightly under 16KB since that results in less fragmentation due to heap metadata const size_t kBlockHeader = sizeof(double) > sizeof(void*) ? sizeof(double) : sizeof(void*); // suitable for aligning double & void* on all platforms +// TODO (FFlagLuauGcPagedSweep): when 'next' is removed, 'kBlockHeader' can be used unconditionally +const size_t kGCOHeader = sizeof(GCheader) > kBlockHeader ? sizeof(GCheader) : kBlockHeader; struct SizeClassConfig { @@ -96,6 +106,7 @@ const SizeClassConfig kSizeClassConfig; // metadata for a block is stored in the first pointer of the block #define metadata(block) (*(void**)(block)) +#define freegcolink(block) (*(void**)((char*)block + kGCOHeader)) /* ** About the realloc function: @@ -117,15 +128,22 @@ const SizeClassConfig kSizeClassConfig; struct lua_Page { + // list of pages with free blocks lua_Page* prev; lua_Page* next; + // list of all gco pages + lua_Page* gcolistprev; + lua_Page* gcolistnext; + int busyBlocks; int blockSize; void* freeList; int freeNext; + int pageSize; + union { char data[1]; @@ -141,6 +159,8 @@ l_noret luaM_toobig(lua_State* L) static lua_Page* luaM_newpage(lua_State* L, uint8_t sizeClass) { + LUAU_ASSERT(!FFlag::LuauGcPagedSweep); + global_State* g = L->global; lua_Page* page = (lua_Page*)(*g->frealloc)(L, g->ud, NULL, 0, kPageSize); if (!page) @@ -155,6 +175,9 @@ static lua_Page* luaM_newpage(lua_State* L, uint8_t sizeClass) page->prev = NULL; page->next = NULL; + page->gcolistprev = NULL; + page->gcolistnext = NULL; + page->busyBlocks = 0; page->blockSize = blockSize; @@ -171,8 +194,69 @@ static lua_Page* luaM_newpage(lua_State* L, uint8_t sizeClass) return page; } +static lua_Page* newpage(lua_State* L, lua_Page** gcopageset, int pageSize, int blockSize, int blockCount) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + global_State* g = L->global; + + LUAU_ASSERT(pageSize - int(offsetof(lua_Page, data)) >= blockSize * blockCount); + + lua_Page* page = (lua_Page*)(*g->frealloc)(L, g->ud, NULL, 0, pageSize); + if (!page) + luaD_throw(L, LUA_ERRMEM); + + ASAN_POISON_MEMORY_REGION(page->data, blockSize * blockCount); + + // setup page header + page->prev = NULL; + page->next = NULL; + + page->gcolistprev = NULL; + page->gcolistnext = NULL; + + page->busyBlocks = 0; + page->blockSize = blockSize; + + // note: we start with the last block in the page and move downward + // either order would work, but that way we don't need to store the block count in the page + // additionally, GC stores objects in singly linked lists, and this way the GC lists end up in increasing pointer order + page->freeList = NULL; + page->freeNext = (blockCount - 1) * blockSize; + + page->pageSize = pageSize; + + if (gcopageset) + { + page->gcolistnext = *gcopageset; + if (page->gcolistnext) + page->gcolistnext->gcolistprev = page; + *gcopageset = page; + } + + return page; +} + +static lua_Page* newclasspage(lua_State* L, lua_Page** freepageset, lua_Page** gcopageset, uint8_t sizeClass, bool storeMetadata) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + int blockSize = kSizeClassConfig.sizeOfClass[sizeClass] + (storeMetadata ? kBlockHeader : 0); + int blockCount = (kPageSize - offsetof(lua_Page, data)) / blockSize; + + lua_Page* page = newpage(L, gcopageset, kPageSize, blockSize, blockCount); + + // prepend a page to page freelist (which is empty because we only ever allocate a new page when it is!) + LUAU_ASSERT(!freepageset[sizeClass]); + freepageset[sizeClass] = page; + + return page; +} + static void luaM_freepage(lua_State* L, lua_Page* page, uint8_t sizeClass) { + LUAU_ASSERT(!FFlag::LuauGcPagedSweep); + global_State* g = L->global; // remove page from freelist @@ -188,6 +272,44 @@ static void luaM_freepage(lua_State* L, lua_Page* page, uint8_t sizeClass) (*g->frealloc)(L, g->ud, page, kPageSize, 0); } +static void freepage(lua_State* L, lua_Page** gcopageset, lua_Page* page) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + global_State* g = L->global; + + if (gcopageset) + { + // remove page from alllist + if (page->gcolistnext) + page->gcolistnext->gcolistprev = page->gcolistprev; + + if (page->gcolistprev) + page->gcolistprev->gcolistnext = page->gcolistnext; + else if (*gcopageset == page) + *gcopageset = page->gcolistnext; + } + + // so long + (*g->frealloc)(L, g->ud, page, page->pageSize, 0); +} + +static void freeclasspage(lua_State* L, lua_Page** freepageset, lua_Page** gcopageset, lua_Page* page, uint8_t sizeClass) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + // remove page from freelist + if (page->next) + page->next->prev = page->prev; + + if (page->prev) + page->prev->next = page->next; + else if (freepageset[sizeClass] == page) + freepageset[sizeClass] = page->next; + + freepage(L, gcopageset, page); +} + static void* luaM_newblock(lua_State* L, int sizeClass) { global_State* g = L->global; @@ -195,7 +317,12 @@ static void* luaM_newblock(lua_State* L, int sizeClass) // slow path: no page in the freelist, allocate a new one if (!page) - page = luaM_newpage(L, sizeClass); + { + if (FFlag::LuauGcPagedSweep) + page = newclasspage(L, g->freepages, NULL, sizeClass, true); + else + page = luaM_newpage(L, sizeClass); + } LUAU_ASSERT(!page->prev); LUAU_ASSERT(page->freeList || page->freeNext >= 0); @@ -236,6 +363,55 @@ static void* luaM_newblock(lua_State* L, int sizeClass) return (char*)block + kBlockHeader; } +static void* luaM_newgcoblock(lua_State* L, int sizeClass) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + global_State* g = L->global; + lua_Page* page = g->freegcopages[sizeClass]; + + // slow path: no page in the freelist, allocate a new one + if (!page) + page = newclasspage(L, g->freegcopages, &g->allgcopages, sizeClass, false); + + LUAU_ASSERT(!page->prev); + LUAU_ASSERT(page->freeList || page->freeNext >= 0); + LUAU_ASSERT(page->blockSize == kSizeClassConfig.sizeOfClass[sizeClass]); + + void* block; + + if (page->freeNext >= 0) + { + block = &page->data + page->freeNext; + ASAN_UNPOISON_MEMORY_REGION(block, page->blockSize); + + page->freeNext -= page->blockSize; + page->busyBlocks++; + } + else + { + // when separate block metadata is not used, free list link is stored inside the block data itself + block = (char*)page->freeList - kGCOHeader; + + ASAN_UNPOISON_MEMORY_REGION((char*)block + kGCOHeader, page->blockSize - kGCOHeader); + + page->freeList = freegcolink(block); + page->busyBlocks++; + } + + // if we allocate the last block out of a page, we need to remove it from free list + if (!page->freeList && page->freeNext < 0) + { + g->freegcopages[sizeClass] = page->next; + if (page->next) + page->next->prev = NULL; + page->next = NULL; + } + + // the user data is right after the metadata + return (char*)block; +} + static void luaM_freeblock(lua_State* L, int sizeClass, void* block) { global_State* g = L->global; @@ -270,12 +446,45 @@ static void luaM_freeblock(lua_State* L, int sizeClass, void* block) // if it's the last block in the page, we don't need the page if (page->busyBlocks == 0) - luaM_freepage(L, page, sizeClass); + { + if (FFlag::LuauGcPagedSweep) + freeclasspage(L, g->freepages, NULL, page, sizeClass); + else + luaM_freepage(L, page, sizeClass); + } +} + +static void luaM_freegcoblock(lua_State* L, int sizeClass, void* block, lua_Page* page) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + global_State* g = L->global; + + // if the page wasn't in the page free list, it should be now since it got a block! + if (!page->freeList && page->freeNext < 0) + { + LUAU_ASSERT(!page->prev); + LUAU_ASSERT(!page->next); + + page->next = g->freegcopages[sizeClass]; + if (page->next) + page->next->prev = page; + g->freegcopages[sizeClass] = page; + } + + // when separate block metadata is not used, free list link is stored inside the block data itself + freegcolink(block) = page->freeList; + page->freeList = (char*)block + kGCOHeader; + + ASAN_POISON_MEMORY_REGION((char*)block + kGCOHeader, page->blockSize - kGCOHeader); + + page->busyBlocks--; + + // if it's the last block in the page, we don't need the page + if (page->busyBlocks == 0) + freeclasspage(L, g->freegcopages, &g->allgcopages, page, sizeClass); } -/* -** generic allocation routines. -*/ void* luaM_new_(lua_State* L, size_t nsize, uint8_t memcat) { global_State* g = L->global; @@ -292,6 +501,43 @@ void* luaM_new_(lua_State* L, size_t nsize, uint8_t memcat) return block; } +GCObject* luaM_newgco_(lua_State* L, size_t nsize, uint8_t memcat) +{ + if (!FFlag::LuauGcPagedSweep) + return (GCObject*)luaM_new_(L, nsize, memcat); + + global_State* g = L->global; + + int nclass = sizeclass(nsize); + + void* block = NULL; + + if (nclass >= 0) + { + LUAU_ASSERT(nsize > 8); + + block = luaM_newgcoblock(L, nclass); + } + else + { + lua_Page* page = newpage(L, &g->allgcopages, offsetof(lua_Page, data) + int(nsize), int(nsize), 1); + + block = &page->data; + ASAN_UNPOISON_MEMORY_REGION(block, page->blockSize); + + page->freeNext -= page->blockSize; + page->busyBlocks++; + } + + if (block == NULL && nsize > 0) + luaD_throw(L, LUA_ERRMEM); + + g->totalbytes += nsize; + g->memcatbytes[memcat] += nsize; + + return (GCObject*)block; +} + void luaM_free_(lua_State* L, void* block, size_t osize, uint8_t memcat) { global_State* g = L->global; @@ -308,6 +554,36 @@ void luaM_free_(lua_State* L, void* block, size_t osize, uint8_t memcat) g->memcatbytes[memcat] -= osize; } +void luaM_freegco_(lua_State* L, GCObject* block, size_t osize, uint8_t memcat, lua_Page* page) +{ + if (!FFlag::LuauGcPagedSweep) + { + luaM_free_(L, block, osize, memcat); + return; + } + + global_State* g = L->global; + LUAU_ASSERT((osize == 0) == (block == NULL)); + + int oclass = sizeclass(osize); + + if (oclass >= 0) + { + block->gch.tt = LUA_TNIL; + + luaM_freegcoblock(L, oclass, block, page); + } + else + { + LUAU_ASSERT(page->busyBlocks == 1); + + freepage(L, &g->allgcopages, page); + } + + g->totalbytes -= osize; + g->memcatbytes[memcat] -= osize; +} + void* luaM_realloc_(lua_State* L, void* block, size_t osize, size_t nsize, uint8_t memcat) { global_State* g = L->global; @@ -344,3 +620,64 @@ void* luaM_realloc_(lua_State* L, void* block, size_t osize, size_t nsize, uint8 g->memcatbytes[memcat] += nsize - osize; return result; } + +void luaM_getpagewalkinfo(lua_Page* page, char** start, char** end, int* busyBlocks, int* blockSize) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + int blockCount = (page->pageSize - offsetof(lua_Page, data)) / page->blockSize; + + *start = page->data + page->freeNext + page->blockSize; + *end = page->data + blockCount * page->blockSize; + *busyBlocks = page->busyBlocks; + *blockSize = page->blockSize; +} + +lua_Page* luaM_getnextgcopage(lua_Page* page) +{ + return page->gcolistnext; +} + +void luaM_visitpage(lua_Page* page, void* context, bool (*visitor)(void* context, lua_Page* page, GCObject* gco)) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + char* start; + char* end; + int busyBlocks; + int blockSize; + luaM_getpagewalkinfo(page, &start, &end, &busyBlocks, &blockSize); + + for (char* pos = start; pos != end; pos += blockSize) + { + GCObject* gco = (GCObject*)pos; + + // skip memory blocks that are already freed + if (gco->gch.tt == LUA_TNIL) + continue; + + // when true is returned it means that the element was deleted + if (visitor(context, page, gco)) + { + // if the last block was removed, page would be removed as well + if (--busyBlocks == 0) + break; + } + } +} + +void luaM_visitgco(lua_State* L, void* context, bool (*visitor)(void* context, lua_Page* page, GCObject* gco)) +{ + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + global_State* g = L->global; + + for (lua_Page* curr = g->allgcopages; curr;) + { + lua_Page* next = curr->gcolistnext; // page blockvisit might destroy the page + + luaM_visitpage(curr, context, visitor); + + curr = next; + } +} diff --git a/VM/src/lmem.h b/VM/src/lmem.h index f526a1b..1bfe48f 100644 --- a/VM/src/lmem.h +++ b/VM/src/lmem.h @@ -4,8 +4,15 @@ #include "lua.h" +struct lua_Page; +union GCObject; + +// TODO: remove with FFlagLuauGcPagedSweep and rename luaM_newgco to luaM_new #define luaM_new(L, t, size, memcat) cast_to(t*, luaM_new_(L, size, memcat)) +#define luaM_newgco(L, t, size, memcat) cast_to(t*, luaM_newgco_(L, size, memcat)) +// TODO: remove with FFlagLuauGcPagedSweep and rename luaM_freegco to luaM_free #define luaM_free(L, p, size, memcat) luaM_free_(L, (p), size, memcat) +#define luaM_freegco(L, p, size, memcat, page) luaM_freegco_(L, obj2gco(p), size, memcat, page) #define luaM_arraysize_(n, e) ((cast_to(size_t, (n)) <= SIZE_MAX / (e)) ? (n) * (e) : (luaM_toobig(L), SIZE_MAX)) @@ -15,7 +22,15 @@ ((v) = cast_to(t*, luaM_realloc_(L, v, (oldn) * sizeof(t), luaM_arraysize_(n, sizeof(t)), memcat))) LUAI_FUNC void* luaM_new_(lua_State* L, size_t nsize, uint8_t memcat); +LUAI_FUNC GCObject* luaM_newgco_(lua_State* L, size_t nsize, uint8_t memcat); LUAI_FUNC void luaM_free_(lua_State* L, void* block, size_t osize, uint8_t memcat); +LUAI_FUNC void luaM_freegco_(lua_State* L, GCObject* block, size_t osize, uint8_t memcat, lua_Page* page); LUAI_FUNC void* luaM_realloc_(lua_State* L, void* block, size_t osize, size_t nsize, uint8_t memcat); LUAI_FUNC l_noret luaM_toobig(lua_State* L); + +LUAI_FUNC void luaM_getpagewalkinfo(lua_Page* page, char** start, char** end, int* busyBlocks, int* blockSize); +LUAI_FUNC lua_Page* luaM_getnextgcopage(lua_Page* page); + +LUAI_FUNC void luaM_visitpage(lua_Page* page, void* context, bool (*visitor)(void* context, lua_Page* page, GCObject* gco)); +LUAI_FUNC void luaM_visitgco(lua_State* L, void* context, bool (*visitor)(void* context, lua_Page* page, GCObject* gco)); diff --git a/VM/src/lobject.h b/VM/src/lobject.h index b642cf7..57ffd82 100644 --- a/VM/src/lobject.h +++ b/VM/src/lobject.h @@ -11,12 +11,11 @@ typedef union GCObject GCObject; /* -** Common Header for all collectible objects (in macro form, to be -** included in other objects) +** Common Header for all collectible objects (in macro form, to be included in other objects) */ // clang-format off #define CommonHeader \ - GCObject* next; \ + GCObject* next; /* TODO: remove with FFlagLuauGcPagedSweep */ \ uint8_t tt; uint8_t marked; uint8_t memcat // clang-format on @@ -229,8 +228,10 @@ typedef TValue* StkId; /* index to stack elements */ typedef struct TString { CommonHeader; + // 1 byte padding int16_t atom; + // 2 byte padding unsigned int hash; unsigned int len; @@ -314,14 +315,21 @@ typedef struct LocVar typedef struct UpVal { CommonHeader; + // 1 (x86) or 5 (x64) byte padding TValue* v; /* points to stack or to its own value */ union { TValue value; /* the value (when closed) */ struct - { /* double linked list (when open) */ + { + /* global double linked list (when open) */ struct UpVal* prev; struct UpVal* next; + + /* thread double linked list (when open) */ + // TODO: when FFlagLuauGcPagedSweep is removed, old outer 'next' value will be placed here + /* note: this is the location of a pointer to this upvalue in the previous element that can be either an UpVal or a lua_State */ + struct UpVal** threadprev; } l; } u; } UpVal; diff --git a/VM/src/lstate.cpp b/VM/src/lstate.cpp index 24e9706..6762c63 100644 --- a/VM/src/lstate.cpp +++ b/VM/src/lstate.cpp @@ -10,6 +10,8 @@ #include "ldo.h" #include "ldebug.h" +LUAU_FASTFLAG(LuauGcPagedSweep) + /* ** Main thread combines a thread state and the global state */ @@ -86,14 +88,21 @@ static void close_state(lua_State* L) global_State* g = L->global; luaF_close(L, L->stack); /* close all upvalues for this thread */ luaC_freeall(L); /* collect all objects */ - LUAU_ASSERT(g->rootgc == obj2gco(L)); + if (!FFlag::LuauGcPagedSweep) + LUAU_ASSERT(g->rootgc == obj2gco(L)); LUAU_ASSERT(g->strbufgc == NULL); LUAU_ASSERT(g->strt.nuse == 0); luaM_freearray(L, L->global->strt.hash, L->global->strt.size, TString*, 0); freestack(L, L); - LUAU_ASSERT(g->totalbytes == sizeof(LG)); for (int i = 0; i < LUA_SIZECLASSES; i++) + { LUAU_ASSERT(g->freepages[i] == NULL); + if (FFlag::LuauGcPagedSweep) + LUAU_ASSERT(g->freegcopages[i] == NULL); + } + if (FFlag::LuauGcPagedSweep) + LUAU_ASSERT(g->allgcopages == NULL); + LUAU_ASSERT(g->totalbytes == sizeof(LG)); LUAU_ASSERT(g->memcatbytes[0] == sizeof(LG)); for (int i = 1; i < LUA_MEMORY_CATEGORIES; i++) LUAU_ASSERT(g->memcatbytes[i] == 0); @@ -102,7 +111,7 @@ static void close_state(lua_State* L) lua_State* luaE_newthread(lua_State* L) { - lua_State* L1 = luaM_new(L, lua_State, sizeof(lua_State), L->activememcat); + lua_State* L1 = luaM_newgco(L, lua_State, sizeof(lua_State), L->activememcat); luaC_link(L, L1, LUA_TTHREAD); preinit_state(L1, L->global); L1->activememcat = L->activememcat; // inherit the active memory category @@ -113,7 +122,7 @@ lua_State* luaE_newthread(lua_State* L) return L1; } -void luaE_freethread(lua_State* L, lua_State* L1) +void luaE_freethread(lua_State* L, lua_State* L1, lua_Page* page) { luaF_close(L1, L1->stack); /* close all upvalues for this thread */ LUAU_ASSERT(L1->openupval == NULL); @@ -121,7 +130,7 @@ void luaE_freethread(lua_State* L, lua_State* L1) if (g->cb.userthread) g->cb.userthread(NULL, L1); freestack(L, L1); - luaM_free(L, L1, sizeof(lua_State), L1->memcat); + luaM_freegco(L, L1, sizeof(lua_State), L1->memcat, page); } void lua_resetthread(lua_State* L) @@ -162,7 +171,8 @@ lua_State* lua_newstate(lua_Alloc f, void* ud) return NULL; L = (lua_State*)l; g = &((LG*)L)->g; - L->next = NULL; + if (!FFlag::LuauGcPagedSweep) + L->next = NULL; L->tt = LUA_TTHREAD; L->marked = g->currentwhite = bit2mask(WHITE0BIT, FIXEDBIT); L->memcat = 0; @@ -185,9 +195,11 @@ lua_State* lua_newstate(lua_Alloc f, void* ud) g->strt.hash = NULL; setnilvalue(registry(L)); g->gcstate = GCSpause; - g->rootgc = obj2gco(L); + if (!FFlag::LuauGcPagedSweep) + g->rootgc = obj2gco(L); g->sweepstrgc = 0; - g->sweepgc = &g->rootgc; + if (!FFlag::LuauGcPagedSweep) + g->sweepgc = &g->rootgc; g->gray = NULL; g->grayagain = NULL; g->weak = NULL; @@ -197,7 +209,16 @@ lua_State* lua_newstate(lua_Alloc f, void* ud) g->gcstepmul = LUAI_GCSTEPMUL; g->gcstepsize = LUAI_GCSTEPSIZE << 10; for (i = 0; i < LUA_SIZECLASSES; i++) + { g->freepages[i] = NULL; + if (FFlag::LuauGcPagedSweep) + g->freegcopages[i] = NULL; + } + if (FFlag::LuauGcPagedSweep) + { + g->allgcopages = NULL; + g->sweepgcopage = NULL; + } for (i = 0; i < LUA_T_COUNT; i++) g->mt[i] = NULL; for (i = 0; i < LUA_UTAG_LIMIT; i++) diff --git a/VM/src/lstate.h b/VM/src/lstate.h index 5637988..080f002 100644 --- a/VM/src/lstate.h +++ b/VM/src/lstate.h @@ -22,7 +22,7 @@ typedef struct stringtable { - GCObject** hash; + TString** hash; uint32_t nuse; /* number of elements */ int size; } stringtable; @@ -149,13 +149,15 @@ typedef struct global_State int sweepstrgc; /* position of sweep in `strt' */ + // TODO: remove with FFlagLuauGcPagedSweep GCObject* rootgc; /* list of all collectable objects */ + // TODO: remove with FFlagLuauGcPagedSweep GCObject** sweepgc; /* position of sweep in `rootgc' */ GCObject* gray; /* list of gray objects */ GCObject* grayagain; /* list of objects to be traversed atomically */ GCObject* weak; /* list of weak tables (to be cleared) */ - GCObject* strbufgc; // list of all string buffer objects + TString* strbufgc; // list of all string buffer objects size_t GCthreshold; // when totalbytes > GCthreshold; run GC step @@ -164,7 +166,10 @@ typedef struct global_State int gcstepmul; // see LUAI_GCSTEPMUL int gcstepsize; // see LUAI_GCSTEPSIZE - struct lua_Page* freepages[LUA_SIZECLASSES]; /* free page linked list for each size class */ + struct lua_Page* freepages[LUA_SIZECLASSES]; // free page linked list for each size class for non-collectable objects + struct lua_Page* freegcopages[LUA_SIZECLASSES]; // free page linked list for each size class for collectable objects + struct lua_Page* allgcopages; // page linked list with all pages for all classes + struct lua_Page* sweepgcopage; // position of the sweep in `allgcopages' size_t memcatbytes[LUA_MEMORY_CATEGORIES]; /* total amount of memory used by each memory category */ @@ -231,7 +236,7 @@ struct lua_State TValue l_gt; /* table of globals */ TValue env; /* temporary place for environments */ - GCObject* openupval; /* list of open upvalues in this stack */ + UpVal* openupval; /* list of open upvalues in this stack */ GCObject* gclist; TString* namecall; /* when invoked from Luau using NAMECALL, what method do we need to invoke? */ @@ -268,4 +273,4 @@ union GCObject #define obj2gco(v) check_exp(iscollectable(v), cast_to(GCObject*, (v) + 0)) LUAI_FUNC lua_State* luaE_newthread(lua_State* L); -LUAI_FUNC void luaE_freethread(lua_State* L, lua_State* L1); +LUAI_FUNC void luaE_freethread(lua_State* L, lua_State* L1, struct lua_Page* page); diff --git a/VM/src/lstring.cpp b/VM/src/lstring.cpp index a9e90d1..cb22cc2 100644 --- a/VM/src/lstring.cpp +++ b/VM/src/lstring.cpp @@ -7,6 +7,8 @@ #include +LUAU_FASTFLAG(LuauGcPagedSweep) + unsigned int luaS_hash(const char* str, size_t len) { // Note that this hashing algorithm is replicated in BytecodeBuilder.cpp, BytecodeBuilder::getStringHash @@ -44,26 +46,25 @@ unsigned int luaS_hash(const char* str, size_t len) void luaS_resize(lua_State* L, int newsize) { - GCObject** newhash; - stringtable* tb; - int i; if (L->global->gcstate == GCSsweepstring) return; /* cannot resize during GC traverse */ - newhash = luaM_newarray(L, newsize, GCObject*, 0); - tb = &L->global->strt; - for (i = 0; i < newsize; i++) + TString** newhash = luaM_newarray(L, newsize, TString*, 0); + stringtable* tb = &L->global->strt; + for (int i = 0; i < newsize; i++) newhash[i] = NULL; /* rehash */ - for (i = 0; i < tb->size; i++) + for (int i = 0; i < tb->size; i++) { - GCObject* p = tb->hash[i]; + TString* p = tb->hash[i]; while (p) { /* for each node in the list */ - GCObject* next = p->gch.next; /* save next */ - unsigned int h = gco2ts(p)->hash; + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + TString* next = (TString*)p->next; /* save next */ + unsigned int h = p->hash; int h1 = lmod(h, newsize); /* new position */ LUAU_ASSERT(cast_int(h % newsize) == lmod(h, newsize)); - p->gch.next = newhash[h1]; /* chain it */ + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + p->next = (GCObject*)newhash[h1]; /* chain it */ newhash[h1] = p; p = next; } @@ -79,7 +80,7 @@ static TString* newlstr(lua_State* L, const char* str, size_t l, unsigned int h) stringtable* tb; if (l > MAXSSIZE) luaM_toobig(L); - ts = luaM_new(L, TString, sizestring(l), L->activememcat); + ts = luaM_newgco(L, TString, sizestring(l), L->activememcat); ts->len = unsigned(l); ts->hash = h; ts->marked = luaC_white(L->global); @@ -90,8 +91,9 @@ static TString* newlstr(lua_State* L, const char* str, size_t l, unsigned int h) ts->atom = L->global->cb.useratom ? L->global->cb.useratom(ts->data, l) : -1; tb = &L->global->strt; h = lmod(h, tb->size); - ts->next = tb->hash[h]; /* chain new entry */ - tb->hash[h] = obj2gco(ts); + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the case will not be required + ts->next = (GCObject*)tb->hash[h]; /* chain new entry */ + tb->hash[h] = ts; tb->nuse++; if (tb->nuse > cast_to(uint32_t, tb->size) && tb->size <= INT_MAX / 2) luaS_resize(L, tb->size * 2); /* too crowded */ @@ -101,28 +103,41 @@ static TString* newlstr(lua_State* L, const char* str, size_t l, unsigned int h) static void linkstrbuf(lua_State* L, TString* ts) { global_State* g = L->global; - GCObject* o = obj2gco(ts); - o->gch.next = g->strbufgc; - g->strbufgc = o; - o->gch.marked = luaC_white(g); + + if (FFlag::LuauGcPagedSweep) + { + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + ts->next = (GCObject*)g->strbufgc; + g->strbufgc = ts; + ts->marked = luaC_white(g); + } + else + { + GCObject* o = obj2gco(ts); + o->gch.next = (GCObject*)g->strbufgc; + g->strbufgc = gco2ts(o); + o->gch.marked = luaC_white(g); + } } static void unlinkstrbuf(lua_State* L, TString* ts) { global_State* g = L->global; - GCObject** p = &g->strbufgc; + TString** p = &g->strbufgc; - while (GCObject* curr = *p) + while (TString* curr = *p) { - if (curr == obj2gco(ts)) + if (curr == ts) { - *p = curr->gch.next; + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + *p = (TString*)curr->next; return; } else { - p = &curr->gch.next; + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + p = (TString**)&curr->next; } } @@ -134,7 +149,7 @@ TString* luaS_bufstart(lua_State* L, size_t size) if (size > MAXSSIZE) luaM_toobig(L); - TString* ts = luaM_new(L, TString, sizestring(size), L->activememcat); + TString* ts = luaM_newgco(L, TString, sizestring(size), L->activememcat); ts->tt = LUA_TSTRING; ts->memcat = L->activememcat; @@ -152,15 +167,14 @@ TString* luaS_buffinish(lua_State* L, TString* ts) int bucket = lmod(h, tb->size); // search if we already have this string in the hash table - for (GCObject* o = tb->hash[bucket]; o != NULL; o = o->gch.next) + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + for (TString* el = tb->hash[bucket]; el != NULL; el = (TString*)el->next) { - TString* el = gco2ts(o); - if (el->len == ts->len && memcmp(el->data, ts->data, ts->len) == 0) { // string may be dead - if (isdead(L->global, o)) - changewhite(o); + if (isdead(L->global, obj2gco(el))) + changewhite(obj2gco(el)); return el; } @@ -173,8 +187,9 @@ TString* luaS_buffinish(lua_State* L, TString* ts) // Complete string object ts->atom = L->global->cb.useratom ? L->global->cb.useratom(ts->data, ts->len) : -1; - ts->next = tb->hash[bucket]; // chain new entry - tb->hash[bucket] = obj2gco(ts); + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + ts->next = (GCObject*)tb->hash[bucket]; // chain new entry + tb->hash[bucket] = ts; tb->nuse++; if (tb->nuse > cast_to(uint32_t, tb->size) && tb->size <= INT_MAX / 2) @@ -185,24 +200,63 @@ TString* luaS_buffinish(lua_State* L, TString* ts) TString* luaS_newlstr(lua_State* L, const char* str, size_t l) { - GCObject* o; unsigned int h = luaS_hash(str, l); - for (o = L->global->strt.hash[lmod(h, L->global->strt.size)]; o != NULL; o = o->gch.next) + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + for (TString* el = L->global->strt.hash[lmod(h, L->global->strt.size)]; el != NULL; el = (TString*)el->next) { - TString* ts = gco2ts(o); - if (ts->len == l && (memcmp(str, getstr(ts), l) == 0)) + if (el->len == l && (memcmp(str, getstr(el), l) == 0)) { /* string may be dead */ - if (isdead(L->global, o)) - changewhite(o); - return ts; + if (isdead(L->global, obj2gco(el))) + changewhite(obj2gco(el)); + return el; } } return newlstr(L, str, l, h); /* not found */ } -void luaS_free(lua_State* L, TString* ts) +static bool unlinkstr(lua_State* L, TString* ts) { - L->global->strt.nuse--; - luaM_free(L, ts, sizestring(ts->len), ts->memcat); + LUAU_ASSERT(FFlag::LuauGcPagedSweep); + + global_State* g = L->global; + + TString** p = &g->strt.hash[lmod(ts->hash, g->strt.size)]; + + while (TString* curr = *p) + { + if (curr == ts) + { + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + *p = (TString*)curr->next; + return true; + } + else + { + // TODO (FFlagLuauGcPagedSweep): 'next' type will change after removal of the flag and the cast will not be required + p = (TString**)&curr->next; + } + } + + return false; +} + +void luaS_free(lua_State* L, TString* ts, lua_Page* page) +{ + if (FFlag::LuauGcPagedSweep) + { + // Unchain from the string table + if (!unlinkstr(L, ts)) + unlinkstrbuf(L, ts); // An unlikely scenario when we have a string buffer on our hands + else + L->global->strt.nuse--; + + luaM_freegco(L, ts, sizestring(ts->len), ts->memcat, page); + } + else + { + L->global->strt.nuse--; + + luaM_free(L, ts, sizestring(ts->len), ts->memcat); + } } diff --git a/VM/src/lstring.h b/VM/src/lstring.h index 3fd0bd3..290b64d 100644 --- a/VM/src/lstring.h +++ b/VM/src/lstring.h @@ -20,7 +20,7 @@ LUAI_FUNC unsigned int luaS_hash(const char* str, size_t len); LUAI_FUNC void luaS_resize(lua_State* L, int newsize); LUAI_FUNC TString* luaS_newlstr(lua_State* L, const char* str, size_t l); -LUAI_FUNC void luaS_free(lua_State* L, TString* ts); +LUAI_FUNC void luaS_free(lua_State* L, TString* ts, struct lua_Page* page); LUAI_FUNC TString* luaS_bufstart(lua_State* L, size_t size); LUAI_FUNC TString* luaS_buffinish(lua_State* L, TString* ts); diff --git a/VM/src/ltable.cpp b/VM/src/ltable.cpp index 83b59f3..c57374e 100644 --- a/VM/src/ltable.cpp +++ b/VM/src/ltable.cpp @@ -424,7 +424,7 @@ static void rehash(lua_State* L, Table* t, const TValue* ek) Table* luaH_new(lua_State* L, int narray, int nhash) { - Table* t = luaM_new(L, Table, sizeof(Table), L->activememcat); + Table* t = luaM_newgco(L, Table, sizeof(Table), L->activememcat); luaC_link(L, t, LUA_TTABLE); t->metatable = NULL; t->flags = cast_byte(~0); @@ -443,12 +443,12 @@ Table* luaH_new(lua_State* L, int narray, int nhash) return t; } -void luaH_free(lua_State* L, Table* t) +void luaH_free(lua_State* L, Table* t, lua_Page* page) { if (t->node != dummynode) luaM_freearray(L, t->node, sizenode(t), LuaNode, t->memcat); luaM_freearray(L, t->array, t->sizearray, TValue, t->memcat); - luaM_free(L, t, sizeof(Table), t->memcat); + luaM_freegco(L, t, sizeof(Table), t->memcat, page); } static LuaNode* getfreepos(Table* t) @@ -741,7 +741,7 @@ int luaH_getn(Table* t) Table* luaH_clone(lua_State* L, Table* tt) { - Table* t = luaM_new(L, Table, sizeof(Table), L->activememcat); + Table* t = luaM_newgco(L, Table, sizeof(Table), L->activememcat); luaC_link(L, t, LUA_TTABLE); t->metatable = tt->metatable; t->flags = tt->flags; diff --git a/VM/src/ltable.h b/VM/src/ltable.h index 4506144..e8413c8 100644 --- a/VM/src/ltable.h +++ b/VM/src/ltable.h @@ -20,7 +20,7 @@ LUAI_FUNC TValue* luaH_set(lua_State* L, Table* t, const TValue* key); LUAI_FUNC Table* luaH_new(lua_State* L, int narray, int lnhash); LUAI_FUNC void luaH_resizearray(lua_State* L, Table* t, int nasize); LUAI_FUNC void luaH_resizehash(lua_State* L, Table* t, int nhsize); -LUAI_FUNC void luaH_free(lua_State* L, Table* t); +LUAI_FUNC void luaH_free(lua_State* L, Table* t, struct lua_Page* page); LUAI_FUNC int luaH_next(lua_State* L, Table* t, StkId key); LUAI_FUNC int luaH_getn(Table* t); LUAI_FUNC Table* luaH_clone(lua_State* L, Table* tt); diff --git a/VM/src/ludata.cpp b/VM/src/ludata.cpp index d180c38..758a9bd 100644 --- a/VM/src/ludata.cpp +++ b/VM/src/ludata.cpp @@ -11,7 +11,7 @@ Udata* luaU_newudata(lua_State* L, size_t s, int tag) { if (s > INT_MAX - sizeof(Udata)) luaM_toobig(L); - Udata* u = luaM_new(L, Udata, sizeudata(s), L->activememcat); + Udata* u = luaM_newgco(L, Udata, sizeudata(s), L->activememcat); luaC_link(L, u, LUA_TUSERDATA); u->len = int(s); u->metatable = NULL; @@ -20,7 +20,7 @@ Udata* luaU_newudata(lua_State* L, size_t s, int tag) return u; } -void luaU_freeudata(lua_State* L, Udata* u) +void luaU_freeudata(lua_State* L, Udata* u, lua_Page* page) { LUAU_ASSERT(u->tag < LUA_UTAG_LIMIT || u->tag == UTAG_IDTOR); @@ -33,5 +33,5 @@ void luaU_freeudata(lua_State* L, Udata* u) if (dtor) dtor(u->data); - luaM_free(L, u, sizeudata(u->len), u->memcat); + luaM_freegco(L, u, sizeudata(u->len), u->memcat, page); } diff --git a/VM/src/ludata.h b/VM/src/ludata.h index 59cb85b..ec374c2 100644 --- a/VM/src/ludata.h +++ b/VM/src/ludata.h @@ -10,4 +10,4 @@ #define sizeudata(len) (offsetof(Udata, data) + len) LUAI_FUNC Udata* luaU_newudata(lua_State* L, size_t s, int tag); -LUAI_FUNC void luaU_freeudata(lua_State* L, Udata* u); +LUAI_FUNC void luaU_freeudata(lua_State* L, Udata* u, struct lua_Page* page); diff --git a/VM/src/lvmexecute.cpp b/VM/src/lvmexecute.cpp index cebeeb5..e58ff2a 100644 --- a/VM/src/lvmexecute.cpp +++ b/VM/src/lvmexecute.cpp @@ -496,7 +496,7 @@ static void luau_execute(lua_State* L) Instruction insn = *pc++; StkId ra = VM_REG(LUAU_INSN_A(insn)); - if (L->openupval && gco2uv(L->openupval)->v >= ra) + if (L->openupval && L->openupval->v >= ra) luaF_close(L, ra); VM_NEXT(); } diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index 8eed953..3b0d677 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -756,6 +756,30 @@ RETURN R0 1 )"); } +TEST_CASE("TableSizePredictionLoop") +{ + ScopedFastFlag sff("LuauPredictTableSizeLoop", true); + + CHECK_EQ("\n" + compileFunction0(R"( +local t = {} +for i=1,4 do + t[i] = 0 +end +return t +)"), + R"( +NEWTABLE R0 0 4 +LOADN R3 1 +LOADN R1 4 +LOADN R2 1 +FORNPREP R1 +3 +LOADN R4 0 +SETTABLE R4 R0 R3 +FORNLOOP R1 -3 +RETURN R0 1 +)"); +} + TEST_CASE("ReflectionEnums") { CHECK_EQ("\n" + compileFunction0("return Enum.EasingStyle.Linear"), R"( diff --git a/tests/Linter.test.cpp b/tests/Linter.test.cpp index 1d13df2..5ad06f0 100644 --- a/tests/Linter.test.cpp +++ b/tests/Linter.test.cpp @@ -1396,6 +1396,8 @@ end TEST_CASE_FIXTURE(Fixture, "TableOperations") { + ScopedFastFlag sff("LuauLintTableCreateTable", true); + LintResult result = lintTyped(R"( local t = {} local tt = {} @@ -1416,9 +1418,12 @@ table.insert(t, string.find("hello", "h")) table.move(t, 0, #t, 1, tt) table.move(t, 1, #t, 0, tt) + +table.create(42, {}) +table.create(42, {} :: {}) )"); - REQUIRE_EQ(result.warnings.size(), 8); + REQUIRE_EQ(result.warnings.size(), 10); CHECK_EQ(result.warnings[0].text, "table.insert will insert the value before the last element, which is likely a bug; consider removing the " "second argument or wrap it in parentheses to silence"); CHECK_EQ(result.warnings[1].text, "table.insert will append the value to the table; consider removing the second argument for efficiency"); @@ -1430,6 +1435,8 @@ table.move(t, 1, #t, 0, tt) "table.insert may change behavior if the call returns more than one result; consider adding parentheses around second argument"); CHECK_EQ(result.warnings[6].text, "table.move uses index 0 but arrays are 1-based; did you mean 1 instead?"); CHECK_EQ(result.warnings[7].text, "table.move uses index 0 but arrays are 1-based; did you mean 1 instead?"); + CHECK_EQ(result.warnings[8].text, "table.create with a table literal will reuse the same object for all elements; consider using a for loop instead"); + CHECK_EQ(result.warnings[9].text, "table.create with a table literal will reuse the same object for all elements; consider using a for loop instead"); } TEST_CASE_FIXTURE(Fixture, "DuplicateConditions") diff --git a/tests/Parser.test.cpp b/tests/Parser.test.cpp index e913565..ac81005 100644 --- a/tests/Parser.test.cpp +++ b/tests/Parser.test.cpp @@ -1498,6 +1498,17 @@ return CHECK_EQ(std::string(str->value.data, str->value.size), "\n"); } +TEST_CASE_FIXTURE(Fixture, "parse_error_broken_comment") +{ + ScopedFastFlag luauStartingBrokenComment{"LuauStartingBrokenComment", true}; + + const char* expected = "Expected identifier when parsing expression, got unfinished comment"; + + matchParseError("--[[unfinished work", expected); + matchParseError("--!strict\n--[[unfinished work", expected); + matchParseError("local x = 1 --[[unfinished work", expected); +} + TEST_CASE_FIXTURE(Fixture, "string_literals_escapes_broken") { const char* expected = "String literal contains malformed escape sequence"; @@ -2333,7 +2344,7 @@ TEST_CASE_FIXTURE(Fixture, "capture_broken_comment_at_the_start_of_the_file") ParseOptions options; options.captureComments = true; - ParseResult result = parseEx(R"( + ParseResult result = tryParse(R"( --[[ )", options); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 27cda14..644efed 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -2180,4 +2180,52 @@ b() CHECK_EQ(toString(result.errors[0]), R"(Cannot call non-function t1 where t1 = { @metatable { __call: t1 }, { } })"); } +TEST_CASE_FIXTURE(Fixture, "length_operator_union") +{ + ScopedFastFlag luauLengthOnCompositeType{"LuauLengthOnCompositeType", true}; + + CheckResult result = check(R"( +local x: {number} | {string} +local y = #x + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "length_operator_intersection") +{ + ScopedFastFlag luauLengthOnCompositeType{"LuauLengthOnCompositeType", true}; + + CheckResult result = check(R"( +local x: {number} & {z:string} -- mixed tables are evil +local y = #x + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "length_operator_non_table_union") +{ + ScopedFastFlag luauLengthOnCompositeType{"LuauLengthOnCompositeType", true}; + + CheckResult result = check(R"( +local x: {number} | any | string +local y = #x + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + +TEST_CASE_FIXTURE(Fixture, "length_operator_union_errors") +{ + ScopedFastFlag luauLengthOnCompositeType{"LuauLengthOnCompositeType", true}; + + CheckResult result = check(R"( +local x: {number} | number | string +local y = #x + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index 7a056af..7ee5253 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -5129,4 +5129,33 @@ local c = a or b LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "bound_typepack_promote") +{ + ScopedFastFlag luauCommittingTxnLogFreeTpPromote{"LuauCommittingTxnLogFreeTpPromote", true}; + + // No assertions should trigger + check(R"( +local function p() + local this = {} + this.pf = foo() + function this:IsActive() end + function this:Start(o) end + return this +end + +local function h(tp, o) + ep = tp + tp:Start(o) + tp.pf.Connect(function() + ep:IsActive() + end) +end + +function on() + local t = p() + h(t) +end + )"); +} + TEST_SUITE_END(); diff --git a/tests/conformance/closure.lua b/tests/conformance/closure.lua index f32d5bd..7b05735 100644 --- a/tests/conformance/closure.lua +++ b/tests/conformance/closure.lua @@ -419,5 +419,20 @@ co = coroutine.create(function () return loadstring("return a")() end) +-- large closure size +do + local a1, a2, a3, a4, a5, a6, a7, a8, a9, a0 + local b1, b2, b3, b4, b5, b6, b7, b8, b9, b0 + local c1, c2, c3, c4, c5, c6, c7, c8, c9, c0 + local d1, d2, d3, d4, d5, d6, d7, d8, d9, d0 + + local f = function() + return + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a0 + + b1 + b2 + b3 + b4 + b5 + b6 + b7 + b8 + b9 + b0 + + c1 + c2 + c3 + c4 + c5 + c6 + c7 + c8 + c9 + c0 + + d1 + d2 + d3 + d4 + d5 + d6 + d7 + d8 + d9 + d0 + end +end return 'OK' diff --git a/tests/conformance/gc.lua b/tests/conformance/gc.lua index 6d9eb85..409cd22 100644 --- a/tests/conformance/gc.lua +++ b/tests/conformance/gc.lua @@ -291,4 +291,32 @@ do for i = 1,10 do table.insert(___Glob, newproxy(true)) end end +-- create threads that die together with their unmarked upvalues +do + local t = {} + + for i = 1,100 do + local c = coroutine.wrap(function() + local uv = {i + 1} + local function f() + return uv[1] * 10 + end + coroutine.yield(uv[1]) + uv = {i + 2} + coroutine.yield(f()) + end) + + assert(c() == i + 1) + table.insert(t, c) + end + + for i = 1,100 do + t[i] = nil + end + + collectgarbage() + +end + + return('OK')