From c730a51ca80af5c356ec1b0beaf7ac43a8f6fa6c Mon Sep 17 00:00:00 2001 From: Vighnesh Date: Fri, 5 Apr 2024 10:41:05 -0700 Subject: [PATCH] Sync to upstream/release/620 --- Analysis/include/Luau/Normalize.h | 36 +- Analysis/include/Luau/Subtyping.h | 10 +- Analysis/src/Autocomplete.cpp | 14 +- Analysis/src/ConstraintGenerator.cpp | 14 +- Analysis/src/ConstraintSolver.cpp | 10 +- Analysis/src/EmbeddedBuiltinDefinitions.cpp | 4 +- Analysis/src/Frontend.cpp | 10 +- Analysis/src/Normalize.cpp | 238 +++++++------ Analysis/src/Subtyping.cpp | 26 +- Analysis/src/TypeChecker2.cpp | 133 +++++--- Analysis/src/TypeFamily.cpp | 3 +- Analysis/src/TypeInfer.cpp | 13 +- Analysis/src/Unifier.cpp | 6 +- Analysis/src/Unifier2.cpp | 52 ++- CMakeLists.txt | 28 +- CodeGen/include/Luau/NativeProtoExecData.h | 7 + CodeGen/include/Luau/SharedCodeAllocator.h | 85 +---- CodeGen/src/CodeGen.cpp | 4 +- CodeGen/src/CodeGenContext.cpp | 353 +++++++++++++++++++- CodeGen/src/CodeGenContext.h | 27 +- CodeGen/src/IrLoweringX64.cpp | 150 ++------- CodeGen/src/OptimizeConstProp.cpp | 5 +- CodeGen/src/SharedCodeAllocator.cpp | 118 +++---- VM/src/lfunc.cpp | 111 +++--- VM/src/lvmload.cpp | 127 ++----- tests/Autocomplete.test.cpp | 98 +++++- tests/ClassFixture.cpp | 4 +- tests/Conformance.test.cpp | 3 - tests/NonStrictTypeChecker.test.cpp | 22 ++ tests/Normalize.test.cpp | 2 +- tests/RequireByString.test.cpp | 57 ++++ tests/SharedCodeAllocator.test.cpp | 130 +++---- tests/TypeInfer.aliases.test.cpp | 4 +- tests/TypeInfer.classes.test.cpp | 62 +++- tests/TypeInfer.functions.test.cpp | 34 +- tests/TypeInfer.generics.test.cpp | 2 +- tests/TypeInfer.modules.test.cpp | 4 +- tests/TypeInfer.provisional.test.cpp | 39 +++ tests/TypeInfer.singletons.test.cpp | 30 +- tests/TypeInfer.tables.test.cpp | 6 +- tests/TypeInfer.typestates.test.cpp | 48 ++- tools/faillist.txt | 51 +-- 42 files changed, 1380 insertions(+), 800 deletions(-) diff --git a/Analysis/include/Luau/Normalize.h b/Analysis/include/Luau/Normalize.h index 9d6312a52..9b992a5e1 100644 --- a/Analysis/include/Luau/Normalize.h +++ b/Analysis/include/Luau/Normalize.h @@ -216,6 +216,20 @@ struct NormalizedFunctionType struct NormalizedType; using NormalizedTyvars = std::unordered_map>; +// Operations provided by `Normalizer` can have ternary results: +// 1. The operation returned true. +// 2. The operation returned false. +// 3. They can hit resource limitations, which invalidates _all normalized types_. +enum class NormalizationResult +{ + // The operation returned true or succeeded. + True, + // The operation returned false or failed. + False, + // Resource limits were hit, invalidating all normalized types. + HitLimits, +}; + // A normalized type is either any, unknown, or one of the form P | T | F | G where // * P is a union of primitive types (including singletons, classes and the error type) // * T is a union of table types @@ -366,8 +380,8 @@ class Normalizer void unionFunctions(NormalizedFunctionType& heress, const NormalizedFunctionType& theress); void unionTablesWithTable(TypeIds& heres, TypeId there); void unionTables(TypeIds& heres, const TypeIds& theres); - bool unionNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars = -1); - bool unionNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes, int ignoreSmallerTyvars = -1); + NormalizationResult unionNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars = -1); + NormalizationResult unionNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes, int ignoreSmallerTyvars = -1); // ------- Negations std::optional negateNormal(const NormalizedType& here); @@ -389,19 +403,19 @@ class Normalizer std::optional intersectionOfFunctions(TypeId here, TypeId there); void intersectFunctionsWithFunction(NormalizedFunctionType& heress, TypeId there); void intersectFunctions(NormalizedFunctionType& heress, const NormalizedFunctionType& theress); - bool intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there, Set& seenSetTypes); - bool intersectNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars = -1); - bool intersectNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes); - bool normalizeIntersections(const std::vector& intersections, NormalizedType& outType); + NormalizationResult intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there, Set& seenSetTypes); + NormalizationResult intersectNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars = -1); + NormalizationResult intersectNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes); + NormalizationResult normalizeIntersections(const std::vector& intersections, NormalizedType& outType); // Check for inhabitance - bool isInhabited(TypeId ty); - bool isInhabited(TypeId ty, Set& seen); - bool isInhabited(const NormalizedType* norm); - bool isInhabited(const NormalizedType* norm, Set& seen); + NormalizationResult isInhabited(TypeId ty); + NormalizationResult isInhabited(TypeId ty, Set& seen); + NormalizationResult isInhabited(const NormalizedType* norm); + NormalizationResult isInhabited(const NormalizedType* norm, Set& seen); // Check for intersections being inhabited - bool isIntersectionInhabited(TypeId left, TypeId right); + NormalizationResult isIntersectionInhabited(TypeId left, TypeId right); // -------- Convert back from a normalized type to a type TypeId typeFromNormal(const NormalizedType& norm); diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index a421fc99c..0a2c3c7f1 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -103,6 +103,14 @@ struct SubtypingEnvironment DenseHashMap mappedGenerics{nullptr}; DenseHashMap mappedGenericPacks{nullptr}; + /* + * See the test cyclic_tables_are_assumed_to_be_compatible_with_classes for + * details. + * + * An empty value is equivalent to a nonexistent key. + */ + DenseHashMap substitutions{nullptr}; + DenseHashMap, SubtypingResult, TypePairHash> ephemeralCache{{}}; /// Applies `mappedGenerics` to the given type. @@ -192,7 +200,7 @@ struct Subtyping SubtypingResult isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const MetatableType* superMt); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const ClassType* superClass); - SubtypingResult isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const TableType* superTable); + SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const ClassType* subClass, TypeId superTy, const TableType* superTable); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const FunctionType* subFunction, const FunctionType* superFunction); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const TableType* superTable); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const TableType* superTable); diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index d18ea70d8..d6f0ab833 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -14,7 +14,6 @@ #include LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); -LUAU_FASTFLAGVARIABLE(LuauAutocompleteStringLiteralBounds, false); static const std::unordered_set kStatementStartingKeywords = { "while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; @@ -465,15 +464,12 @@ AutocompleteEntryMap autocompleteModuleTypes(const Module& module, Position posi static void autocompleteStringSingleton(TypeId ty, bool addQuotes, AstNode* node, Position position, AutocompleteEntryMap& result) { - if (FFlag::LuauAutocompleteStringLiteralBounds) + if (position == node->location.begin || position == node->location.end) { - if (position == node->location.begin || position == node->location.end) - { - if (auto str = node->as(); str && str->quoteStyle == AstExprConstantString::Quoted) - return; - else if (node->is()) - return; - } + if (auto str = node->as(); str && str->quoteStyle == AstExprConstantString::Quoted) + return; + else if (node->is()) + return; } auto formatKey = [addQuotes](const std::string& key) { diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 8b8ca062b..4a4c8c3ce 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -738,14 +738,18 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatLocal* stat scope->lvalueTypes[def] = assignee; } - TypePackId resultPack = checkPack(scope, statLocal->values, expectedTypes).tp; - addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack, /*resultIsLValue*/ true}); + TypePackId rvaluePack = checkPack(scope, statLocal->values, expectedTypes).tp; - // Types must flow between whatever annotations were provided and the rhs expression. if (hasAnnotation) - addConstraint(scope, statLocal->location, PackSubtypeConstraint{resultPack, arena->addTypePack(std::move(annotatedTypes))}); + { + TypePackId annotatedPack = arena->addTypePack(std::move(annotatedTypes)); + addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), annotatedPack, /*resultIsLValue*/ true}); + addConstraint(scope, statLocal->location, PackSubtypeConstraint{rvaluePack, annotatedPack}); + } + else + addConstraint(scope, statLocal->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), rvaluePack, /*resultIsLValue*/ true}); - if (statLocal->vars.size == 1 && statLocal->values.size == 1 && firstValueType && scope.get() == rootScope) + if (statLocal->vars.size == 1 && statLocal->values.size == 1 && firstValueType && scope.get() == rootScope && !hasAnnotation) { AstLocal* var = statLocal->vars.data[0]; AstExpr* value = statLocal->values.data[0]; diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 7cbe88cd3..7d337c19b 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -27,7 +27,7 @@ #include LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false); - +LUAU_FASTFLAGVARIABLE(DebugLuauLogBindings, false); LUAU_FASTINTVARIABLE(LuauSolverRecursionLimit, 500); namespace Luau @@ -465,10 +465,8 @@ void ConstraintSolver::run() reduceFamilies(instance, Location{}, TypeFamilyContext{arena, builtinTypes, rootScope, normalizer, NotNull{&iceReporter}, NotNull{&limits}}, false); } - if (FFlag::DebugLuauLogSolver) - { + if (FFlag::DebugLuauLogSolver || FFlag::DebugLuauLogBindings) dumpBindings(rootScope, opts); - } if (logger) { @@ -1761,7 +1759,7 @@ bool ConstraintSolver::tryDispatchUnpack1(NotNull constraint, else { LUAU_ASSERT(resultIsLValue); - unify(constraint, resultTy, srcTy); + unify(constraint, srcTy, resultTy); } unblock(resultTy, constraint->location); @@ -1812,7 +1810,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull number, + time: (time: DateTypeArg?) -> number, date: ((formatString: "*t" | "!*t", time: number?) -> DateTypeResult) & ((formatString: string?, time: number?) -> string), - difftime: @checked (t2: DateTypeResult | number, t1: DateTypeResult | number) -> number, + difftime: (t2: DateTypeResult | number, t1: DateTypeResult | number) -> number, clock: () -> number, } diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 758acef34..1ac76835c 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -38,6 +38,8 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJsonFile, false) LUAU_FASTFLAGVARIABLE(DebugLuauForbidInternalTypes, false) +LUAU_FASTFLAGVARIABLE(DebugLuauForceStrictMode, false) +LUAU_FASTFLAGVARIABLE(DebugLuauForceNonStrictMode, false) namespace Luau { @@ -891,7 +893,13 @@ void Frontend::checkBuildQueueItem(BuildQueueItem& item) SourceNode& sourceNode = *item.sourceNode; const SourceModule& sourceModule = *item.sourceModule; const Config& config = item.config; - Mode mode = sourceModule.mode.value_or(config.mode); + Mode mode; + if (FFlag::DebugLuauForceStrictMode) + mode = Mode::Strict; + else if (FFlag::DebugLuauForceNonStrictMode) + mode = Mode::Nonstrict; + else + mode = sourceModule.mode.value_or(config.mode); ScopePtr environmentScope = item.environmentScope; double timestamp = getTimestamp(); const std::vector& requireCycles = item.requireCycles; diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index d749c6973..4a3d8b6bd 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -421,77 +421,77 @@ static bool isShallowInhabited(const NormalizedType& norm) !get(norm.buffers) || !norm.functions.isNever() || !norm.tables.empty() || !norm.tyvars.empty(); } -bool Normalizer::isInhabited(const NormalizedType* norm) +NormalizationResult Normalizer::isInhabited(const NormalizedType* norm) { Set seen{nullptr}; return isInhabited(norm, seen); } -bool Normalizer::isInhabited(const NormalizedType* norm, Set& seen) +NormalizationResult Normalizer::isInhabited(const NormalizedType* norm, Set& seen) { RecursionCounter _rc(&sharedState->counters.recursionCount); - if (!withinResourceLimits()) - return false; - - // If normalization failed, the type is complex, and so is more likely than not to be inhabited. - if (!norm) - return true; + if (!withinResourceLimits() || !norm) + return NormalizationResult::HitLimits; if (!get(norm->tops) || !get(norm->booleans) || !get(norm->errors) || !get(norm->nils) || !get(norm->numbers) || !get(norm->threads) || !get(norm->buffers) || !norm->classes.isNever() || !norm->strings.isNever() || !norm->functions.isNever()) - return true; + return NormalizationResult::True; for (const auto& [_, intersect] : norm->tyvars) { - if (isInhabited(intersect.get(), seen)) - return true; + NormalizationResult res = isInhabited(intersect.get(), seen); + if (res != NormalizationResult::False) + return res; } for (TypeId table : norm->tables) { - if (isInhabited(table, seen)) - return true; + NormalizationResult res = isInhabited(table, seen); + if (res != NormalizationResult::False) + return res; } - return false; + return NormalizationResult::False; } -bool Normalizer::isInhabited(TypeId ty) +NormalizationResult Normalizer::isInhabited(TypeId ty) { if (cacheInhabitance) { if (bool* result = cachedIsInhabited.find(ty)) - return *result; + return *result ? NormalizationResult::True : NormalizationResult::False; } Set seen{nullptr}; - bool result = isInhabited(ty, seen); + NormalizationResult result = isInhabited(ty, seen); - if (cacheInhabitance) - cachedIsInhabited[ty] = result; + if (cacheInhabitance && result == NormalizationResult::True) + cachedIsInhabited[ty] = true; + else if (cacheInhabitance && result == NormalizationResult::False) + cachedIsInhabited[ty] = false; return result; } -bool Normalizer::isInhabited(TypeId ty, Set& seen) +NormalizationResult Normalizer::isInhabited(TypeId ty, Set& seen) { RecursionCounter _rc(&sharedState->counters.recursionCount); if (!withinResourceLimits()) - return false; + return NormalizationResult::HitLimits; // TODO: use log.follow(ty), CLI-64291 ty = follow(ty); if (get(ty)) - return false; + return NormalizationResult::False; if (!get(ty) && !get(ty) && !get(ty) && !get(ty)) - return true; + return NormalizationResult::True; if (seen.count(ty)) - return true; + return NormalizationResult::True; seen.insert(ty); @@ -503,26 +503,36 @@ bool Normalizer::isInhabited(TypeId ty, Set& seen) { // A table enclosing a read property whose type is uninhabitable is also itself uninhabitable, // but not its write property. That just means the write property doesn't exist, and so is readonly. - if (auto ty = prop.readTy; ty && !isInhabited(*ty, seen)) - return false; + if (auto ty = prop.readTy) + { + NormalizationResult res = isInhabited(*ty, seen); + if (res != NormalizationResult::True) + return res; + } } else { - if (!isInhabited(prop.type(), seen)) - return false; + NormalizationResult res = isInhabited(prop.type(), seen); + if (res != NormalizationResult::True) + return res; } } - return true; + return NormalizationResult::True; } if (const MetatableType* mtv = get(ty)) - return isInhabited(mtv->table, seen) && isInhabited(mtv->metatable, seen); + { + NormalizationResult res = isInhabited(mtv->table, seen); + if (res != NormalizationResult::True) + return res; + return isInhabited(mtv->metatable, seen); + } const NormalizedType* norm = normalize(ty); return isInhabited(norm, seen); } -bool Normalizer::isIntersectionInhabited(TypeId left, TypeId right) +NormalizationResult Normalizer::isIntersectionInhabited(TypeId left, TypeId right) { left = follow(left); right = follow(right); @@ -530,7 +540,7 @@ bool Normalizer::isIntersectionInhabited(TypeId left, TypeId right) if (cacheInhabitance) { if (bool* result = cachedIsInhabitedIntersection.find({left, right})) - return *result; + return *result ? NormalizationResult::True : NormalizationResult::False; } Set seen{nullptr}; @@ -538,18 +548,21 @@ bool Normalizer::isIntersectionInhabited(TypeId left, TypeId right) seen.insert(right); NormalizedType norm{builtinTypes}; - if (!normalizeIntersections({left, right}, norm)) + NormalizationResult res = normalizeIntersections({left, right}, norm); + if (res != NormalizationResult::True) { - if (cacheInhabitance) + if (cacheInhabitance && res == NormalizationResult::False) cachedIsInhabitedIntersection[{left, right}] = false; - return false; + return res; } - bool result = isInhabited(&norm, seen); + NormalizationResult result = isInhabited(&norm, seen); - if (cacheInhabitance) - cachedIsInhabitedIntersection[{left, right}] = result; + if (cacheInhabitance && result == NormalizationResult::True) + cachedIsInhabitedIntersection[{left, right}] = true; + else if (cacheInhabitance && result == NormalizationResult::False) + cachedIsInhabitedIntersection[{left, right}] = false; return result; } @@ -827,7 +840,8 @@ const NormalizedType* Normalizer::normalize(TypeId ty) NormalizedType norm{builtinTypes}; Set seenSetTypes{nullptr}; - if (!unionNormalWithTy(norm, ty, seenSetTypes)) + NormalizationResult res = unionNormalWithTy(norm, ty, seenSetTypes); + if (res != NormalizationResult::True) return nullptr; if (norm.isUnknown()) { @@ -840,7 +854,7 @@ const NormalizedType* Normalizer::normalize(TypeId ty) return result; } -bool Normalizer::normalizeIntersections(const std::vector& intersections, NormalizedType& outType) +NormalizationResult Normalizer::normalizeIntersections(const std::vector& intersections, NormalizedType& outType) { if (!arena) sharedState->iceHandler->ice("Normalizing types outside a module"); @@ -850,14 +864,16 @@ bool Normalizer::normalizeIntersections(const std::vector& intersections Set seenSetTypes{nullptr}; for (auto ty : intersections) { - if (!intersectNormalWithTy(norm, ty, seenSetTypes)) - return false; + NormalizationResult res = intersectNormalWithTy(norm, ty, seenSetTypes); + if (res != NormalizationResult::True) + return res; } - if (!unionNormals(outType, norm)) - return false; + NormalizationResult res = unionNormals(outType, norm); + if (res != NormalizationResult::True) + return res; - return true; + return NormalizationResult::True; } void Normalizer::clearNormal(NormalizedType& norm) @@ -1521,7 +1537,7 @@ void Normalizer::unionTables(TypeIds& heres, const TypeIds& theres) // // And yes, this is essentially a SAT solver hidden inside a typechecker. // That's what you get for having a type system with generics, intersection and union types. -bool Normalizer::unionNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars) +NormalizationResult Normalizer::unionNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars) { TypeId tops = unionOfTops(here.tops, there.tops); if (FFlag::LuauTransitiveSubtyping && get(tops) && (get(here.errors) || get(there.errors))) @@ -1530,7 +1546,7 @@ bool Normalizer::unionNormals(NormalizedType& here, const NormalizedType& there, { clearNormal(here); here.tops = tops; - return true; + return NormalizationResult::True; } for (auto it = there.tyvars.begin(); it != there.tyvars.end(); it++) @@ -1542,10 +1558,15 @@ bool Normalizer::unionNormals(NormalizedType& here, const NormalizedType& there, continue; auto [emplaced, fresh] = here.tyvars.emplace(tyvar, std::make_unique(NormalizedType{builtinTypes})); if (fresh) - if (!unionNormals(*emplaced->second, here, index)) - return false; - if (!unionNormals(*emplaced->second, inter, index)) - return false; + { + NormalizationResult res = unionNormals(*emplaced->second, here, index); + if (res != NormalizationResult::True) + return res; + } + + NormalizationResult res = unionNormals(*emplaced->second, inter, index); + if (res != NormalizationResult::True) + return res; } here.booleans = unionOfBools(here.booleans, there.booleans); @@ -1559,7 +1580,7 @@ bool Normalizer::unionNormals(NormalizedType& here, const NormalizedType& there, here.buffers = (get(there.buffers) ? here.buffers : there.buffers); unionFunctions(here.functions, there.functions); unionTables(here.tables, there.tables); - return true; + return NormalizationResult::True; } bool Normalizer::withinResourceLimits() @@ -1585,11 +1606,11 @@ bool Normalizer::withinResourceLimits() } // See above for an explaination of `ignoreSmallerTyvars`. -bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes, int ignoreSmallerTyvars) +NormalizationResult Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes, int ignoreSmallerTyvars) { RecursionCounter _rc(&sharedState->counters.recursionCount); if (!withinResourceLimits()) - return false; + return NormalizationResult::HitLimits; there = follow(there); @@ -1600,34 +1621,35 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, SetanyType; clearNormal(here); here.tops = tops; - return true; + return NormalizationResult::True; } else if (!FFlag::LuauTransitiveSubtyping && (get(there) || !get(here.tops))) - return true; + return NormalizationResult::True; else if (FFlag::LuauTransitiveSubtyping && (get(there) || get(here.tops))) - return true; + return NormalizationResult::True; else if (FFlag::LuauTransitiveSubtyping && get(there) && get(here.tops)) { here.tops = builtinTypes->anyType; - return true; + return NormalizationResult::True; } else if (const UnionType* utv = get(there)) { if (seenSetTypes.count(there)) - return true; + return NormalizationResult::True; seenSetTypes.insert(there); for (UnionTypeIterator it = begin(utv); it != end(utv); ++it) { - if (!unionNormalWithTy(here, *it, seenSetTypes)) + NormalizationResult res = unionNormalWithTy(here, *it, seenSetTypes); + if (res != NormalizationResult::True) { seenSetTypes.erase(there); - return false; + return res; } } seenSetTypes.erase(there); - return true; + return NormalizationResult::True; } else if (const IntersectionType* itv = get(there)) { @@ -1635,18 +1657,19 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, SetanyType; for (IntersectionTypeIterator it = begin(itv); it != end(itv); ++it) { - if (!intersectNormalWithTy(norm, *it, seenSetTypes)) - return false; + NormalizationResult res = intersectNormalWithTy(norm, *it, seenSetTypes); + if (res != NormalizationResult::True) + return res; } return unionNormals(here, norm); } else if (FFlag::LuauTransitiveSubtyping && get(here.tops)) - return true; + return NormalizationResult::True; else if (get(there) || get(there) || get(there) || get(there) || get(there)) { if (tyvarIndex(there) <= ignoreSmallerTyvars) - return true; + return NormalizationResult::True; NormalizedType inter{builtinTypes}; inter.tops = builtinTypes->unknownType; here.tyvars.insert_or_assign(there, std::make_unique(std::move(inter))); @@ -1714,10 +1737,11 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, Setty); std::optional tn = negateNormal(*thereNormal); if (!tn) - return false; + return NormalizationResult::False; - if (!unionNormals(here, *tn)) - return false; + NormalizationResult res = unionNormals(here, *tn); + if (res != NormalizationResult::True) + return res; } else if (get(there) || get(there)) { @@ -1727,11 +1751,14 @@ bool Normalizer::unionNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes) +NormalizationResult Normalizer::intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there, Set& seenSetTypes) { for (auto it = here.begin(); it != here.end();) { NormalizedType& inter = *it->second; - if (!intersectNormalWithTy(inter, there, seenSetTypes)) - return false; + NormalizationResult res = intersectNormalWithTy(inter, there, seenSetTypes); + if (res != NormalizationResult::True) + return res; if (isShallowInhabited(inter)) ++it; else it = here.erase(it); } - return true; + return NormalizationResult::True; } // See above for an explaination of `ignoreSmallerTyvars`. -bool Normalizer::intersectNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars) +NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars) { if (!get(there.tops)) { here.tops = intersectionOfTops(here.tops, there.tops); - return true; + return NormalizationResult::True; } else if (!get(here.tops)) { @@ -2789,8 +2817,9 @@ bool Normalizer::intersectNormals(NormalizedType& here, const NormalizedType& th auto [found, fresh] = here.tyvars.emplace(tyvar, std::make_unique(NormalizedType{builtinTypes})); if (fresh) { - if (!unionNormals(*found->second, here, index)) - return false; + NormalizationResult res = unionNormals(*found->second, here, index); + if (res != NormalizationResult::True) + return res; } } } @@ -2803,34 +2832,36 @@ bool Normalizer::intersectNormals(NormalizedType& here, const NormalizedType& th auto found = there.tyvars.find(tyvar); if (found == there.tyvars.end()) { - if (!intersectNormals(inter, there, index)) - return false; + NormalizationResult res = intersectNormals(inter, there, index); + if (res != NormalizationResult::True) + return res; } else { - if (!intersectNormals(inter, *found->second, index)) - return false; + NormalizationResult res = intersectNormals(inter, *found->second, index); + if (res != NormalizationResult::True) + return res; } if (isShallowInhabited(inter)) it++; else it = here.tyvars.erase(it); } - return true; + return NormalizationResult::True; } -bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes) +NormalizationResult Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set& seenSetTypes) { RecursionCounter _rc(&sharedState->counters.recursionCount); if (!withinResourceLimits()) - return false; + return NormalizationResult::HitLimits; there = follow(there); if (get(there) || get(there)) { here.tops = intersectionOfTops(here.tops, there); - return true; + return NormalizationResult::True; } else if (!get(here.tops)) { @@ -2841,16 +2872,22 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set(there)) { for (IntersectionTypeIterator it = begin(itv); it != end(itv); ++it) - if (!intersectNormalWithTy(here, *it, seenSetTypes)) - return false; - return true; + { + NormalizationResult res = intersectNormalWithTy(here, *it, seenSetTypes); + if (res != NormalizationResult::True) + return res; + } + return NormalizationResult::True; } else if (get(there) || get(there) || get(there) || get(there) || get(there) || get(there)) @@ -2956,7 +2993,7 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set negated = negateNormal(*normal); if (!negated) - return false; + return NormalizationResult::False; intersectNormals(here, *negated); } else if (const UnionType* itv = get(t)) @@ -2966,7 +3003,7 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set negated = negateNormal(*normalPart); if (!negated) - return false; + return NormalizationResult::False; intersectNormals(here, *negated); } } @@ -2974,13 +3011,13 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set(t)) { // if we're intersecting with `~never`, this is equivalent to intersecting with `unknown` // this is a noop since an intersection with `unknown` is trivial. - return true; + return NormalizationResult::True; } else if (auto nt = get(t)) return intersectNormalWithTy(here, nt->ty, seenSetTypes); @@ -2998,11 +3035,12 @@ bool Normalizer::intersectNormalWithTy(NormalizedType& here, TypeId there, Set(ub, builtinTypes->unknownType); const NormalizedType* nt = normalizer->normalize(upperBound); - if (!nt) + // we say that the result is true if normalization failed because complex types are likely to be inhabited. + NormalizationResult res = nt ? normalizer->isInhabited(nt) : NormalizationResult::True; + + if (!nt || res == NormalizationResult::HitLimits) result.normalizationTooComplex = true; - else if (!normalizer->isInhabited(nt)) + else if (res == NormalizationResult::False) { /* If the normalized upper bound we're mapping to a generic is * uninhabited, then we must consider the subtyping relation not to @@ -433,6 +436,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub subTy = follow(subTy); superTy = follow(superTy); + if (TypeId* subIt = env.substitutions.find(subTy); subIt && *subIt) + subTy = *subIt; + + if (TypeId* superIt = env.substitutions.find(superTy); superIt && *superIt) + superTy = *superIt; + SubtypingResult* cachedResult = resultCache.find({subTy, superTy}); if (cachedResult) return *cachedResult; @@ -612,7 +621,7 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId sub else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p); else if (auto p = get2(subTy, superTy)) - result = isCovariantWith(env, p); + result = isCovariantWith(env, subTy, p.first, superTy, p.second); else if (auto p = get2(subTy, superTy)) result = isCovariantWith(env, p); else if (auto p = get2(subTy, superTy)) @@ -1301,10 +1310,12 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Clas return {isSubclass(subClass, superClass)}; } -SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const TableType* superTable) +SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const ClassType* subClass, TypeId superTy, const TableType* superTable) { SubtypingResult result{true}; + env.substitutions[superTy] = subTy; + for (const auto& [name, prop] : superTable->props) { if (auto classProp = lookupClassProp(subClass, name)) @@ -1312,9 +1323,14 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const Clas result.andAlso(isCovariantWith(env, *classProp, prop, name)); } else - return SubtypingResult{false}; + { + result = {false}; + break; + } } + env.substitutions[superTy] = nullptr; + return result; } diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 922a5671b..c82bb6e9c 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -1352,12 +1352,15 @@ struct TypeChecker2 auto norm = normalizer.normalize(fnTy); if (!norm) reportError(NormalizationTooComplex{}, call->func->location); + auto isInhabited = normalizer.isInhabited(norm); + if (isInhabited == NormalizationResult::HitLimits) + reportError(NormalizationTooComplex{}, call->func->location); if (norm && norm->shouldSuppressErrors()) return; // error suppressing function type! else if (!resolver.ok.empty()) return; // We found a call that works, so this is ok. - else if (!norm || !normalizer.isInhabited(norm)) + else if (!norm || isInhabited == NormalizationResult::False) return; // Ok. Calling an uninhabited type is no-op. else if (!resolver.nonviableOverloads.empty()) { @@ -1802,7 +1805,7 @@ struct TypeChecker2 return leftType; } - bool typesHaveIntersection = normalizer.isIntersectionInhabited(leftType, rightType); + NormalizationResult typesHaveIntersection = normalizer.isIntersectionInhabited(leftType, rightType); if (auto it = kBinaryOpMetamethods.find(expr->op); it != kBinaryOpMetamethods.end()) { std::optional leftMt = getMetatable(leftType, builtinTypes); @@ -1836,11 +1839,11 @@ struct TypeChecker2 // If we're working with things that are not tables, the metatable comparisons above are a little excessive // It's ok for one type to have a meta table and the other to not. In that case, we should fall back on - // checking if the intersection of the types is inhabited. + // checking if the intersection of the types is inhabited. If `typesHaveIntersection` failed due to limits, // TODO: Maybe add more checks here (e.g. for functions, classes, etc) if (!(get(leftType) || get(rightType))) if (!leftMt.has_value() || !rightMt.has_value()) - matches = matches || typesHaveIntersection; + matches = matches || typesHaveIntersection != NormalizationResult::False; if (!matches && isComparison) { @@ -2594,35 +2597,59 @@ struct TypeChecker2 bool foundOneProp = false; std::vector typesMissingTheProp; + // this is `false` if we ever hit the resource limits during any of our uses of `fetch`. + bool normValid = true; + auto fetch = [&](TypeId ty) { - if (!normalizer.isInhabited(ty)) + NormalizationResult result = normalizer.isInhabited(ty); + if (result == NormalizationResult::HitLimits) + normValid = false; + if (result != NormalizationResult::True) return; DenseHashSet seen{nullptr}; - bool found = hasIndexTypeFromType(ty, prop, context, location, seen, astIndexExprType, errors); - foundOneProp |= found; - if (!found) + NormalizationResult found = hasIndexTypeFromType(ty, prop, context, location, seen, astIndexExprType, errors); + + if (found == NormalizationResult::HitLimits) + { + normValid = false; + return; + } + + foundOneProp |= found == NormalizationResult::True; + if (found == NormalizationResult::False) typesMissingTheProp.push_back(ty); }; - fetch(norm->tops); - fetch(norm->booleans); + if (normValid) + fetch(norm->tops); + if (normValid) + fetch(norm->booleans); - for (const auto& [ty, _negations] : norm->classes.classes) + if (normValid) { - fetch(ty); + for (const auto& [ty, _negations] : norm->classes.classes) + { + fetch(ty); + } } - fetch(norm->errors); - fetch(norm->nils); - fetch(norm->numbers); - if (!norm->strings.isNever()) + + if (normValid) + fetch(norm->errors); + if (normValid) + fetch(norm->nils); + if (normValid) + fetch(norm->numbers); + if (normValid && !norm->strings.isNever()) fetch(builtinTypes->stringType); - fetch(norm->threads); + if (normValid) + fetch(norm->threads); for (TypeId ty : norm->tables) - fetch(ty); - if (norm->functions.isTop) + if (normValid) + fetch(ty); + if (normValid && norm->functions.isTop) fetch(builtinTypes->functionType); - else if (!norm->functions.isNever()) + else if (normValid && !norm->functions.isNever()) { if (norm->functions.parts.size() == 1) fetch(norm->functions.parts.front()); @@ -2633,15 +2660,19 @@ struct TypeChecker2 fetch(module->internalTypes.addType(IntersectionType{std::move(parts)})); } } - for (const auto& [tyvar, intersect] : norm->tyvars) + + if (normValid) { - if (get(intersect->tops)) + for (const auto& [tyvar, intersect] : norm->tyvars) { - TypeId ty = normalizer.typeFromNormal(*intersect); - fetch(module->internalTypes.addType(IntersectionType{{tyvar, ty}})); + if (get(intersect->tops)) + { + TypeId ty = normalizer.typeFromNormal(*intersect); + fetch(module->internalTypes.addType(IntersectionType{{tyvar, ty}})); + } + else + fetch(tyvar); } - else - fetch(tyvar); } return {foundOneProp, typesMissingTheProp}; @@ -2695,18 +2726,18 @@ struct TypeChecker2 } } - bool hasIndexTypeFromType(TypeId ty, const std::string& prop, ValueContext context, const Location& location, DenseHashSet& seen, - TypeId astIndexExprType, std::vector& errors) + NormalizationResult hasIndexTypeFromType(TypeId ty, const std::string& prop, ValueContext context, const Location& location, + DenseHashSet& seen, TypeId astIndexExprType, std::vector& errors) { // If we have already encountered this type, we must assume that some // other codepath will do the right thing and signal false if the // property is not present. if (seen.contains(ty)) - return true; + return NormalizationResult::True; seen.insert(ty); if (get(ty) || get(ty) || get(ty)) - return true; + return NormalizationResult::True; if (isString(ty)) { @@ -2718,23 +2749,23 @@ struct TypeChecker2 if (auto tt = getTableType(ty)) { if (findTablePropertyRespectingMeta(builtinTypes, errors, ty, prop, context, location)) - return true; + return NormalizationResult::True; if (tt->indexer) { TypeId indexType = follow(tt->indexer->indexType); if (isPrim(indexType, PrimitiveType::String)) - return true; + return NormalizationResult::True; // If the indexer looks like { [any] : _} - the prop lookup should be allowed! else if (get(indexType) || get(indexType)) - return true; + return NormalizationResult::True; } // if we are in a conditional context, we treat the property as present and `unknown` because // we may be _refining_ `tableTy` to include that property. we will want to revisit this a bit // in the future once luau has support for exact tables since this only applies when inexact. - return inConditional(typeContext); + return inConditional(typeContext) ? NormalizationResult::True : NormalizationResult::False; } else if (const ClassType* cls = get(ty)) { @@ -2743,26 +2774,40 @@ struct TypeChecker2 // is compatible with the indexer's indexType // Construct the intersection and test inhabitedness! if (auto property = lookupClassProp(cls, prop)) - return true; + return NormalizationResult::True; if (cls->indexer) { TypeId inhabitatedTestType = module->internalTypes.addType(IntersectionType{{cls->indexer->indexType, astIndexExprType}}); return normalizer.isInhabited(inhabitatedTestType); } - return false; + return NormalizationResult::False; } else if (const UnionType* utv = get(ty)) - return std::all_of(begin(utv), end(utv), [&](TypeId part) { - return hasIndexTypeFromType(part, prop, context, location, seen, astIndexExprType, errors); - }); + { + for (TypeId part : utv) + { + NormalizationResult result = hasIndexTypeFromType(part, prop, context, location, seen, astIndexExprType, errors); + if (result != NormalizationResult::True) + return result; + } + + return NormalizationResult::True; + } else if (const IntersectionType* itv = get(ty)) - return std::any_of(begin(itv), end(itv), [&](TypeId part) { - return hasIndexTypeFromType(part, prop, context, location, seen, astIndexExprType, errors); - }); + { + for (TypeId part : itv) + { + NormalizationResult result = hasIndexTypeFromType(part, prop, context, location, seen, astIndexExprType, errors); + if (result != NormalizationResult::False) + return result; + } + + return NormalizationResult::False; + } else if (const PrimitiveType* pt = get(ty)) - return inConditional(typeContext) && pt->type == PrimitiveType::Table; + return (inConditional(typeContext) && pt->type == PrimitiveType::Table) ? NormalizationResult::True : NormalizationResult::False; else - return false; + return NormalizationResult::False; } void diagnoseMissingTableKey(UnknownProperty* utk, TypeErrorData& data) const diff --git a/Analysis/src/TypeFamily.cpp b/Analysis/src/TypeFamily.cpp index c120eeb1d..f1ee929a9 100644 --- a/Analysis/src/TypeFamily.cpp +++ b/Analysis/src/TypeFamily.cpp @@ -1124,7 +1124,8 @@ TypeFamilyReductionResult eqFamilyFn( mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, "__eq", Location{}); // if neither type has a metatable entry for `__eq`, then we'll check for inhabitance of the intersection! - if (!mmType && ctx->normalizer->isIntersectionInhabited(lhsTy, rhsTy)) + NormalizationResult intersectInhabited = ctx->normalizer->isIntersectionInhabited(lhsTy, rhsTy); + if (!mmType && intersectInhabited == NormalizationResult::True) return {ctx->builtins->booleanType, false, {}, {}}; // if it's inhabited, everything is okay! else if (!mmType) return {std::nullopt, true, {}, {}}; // if it's not, then this family is irreducible! diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index 653beb0e9..66c3cc20b 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -2653,7 +2653,18 @@ static std::optional areEqComparable(NotNull arena, NotNullisInhabited(n); + switch (normalizer->isInhabited(n)) + { + case NormalizationResult::HitLimits: + return std::nullopt; + case NormalizationResult::False: + return false; + case NormalizationResult::True: + return true; + } + + // n.b. msvc can never figure this stuff out. + LUAU_UNREACHABLE(); } TypeId TypeChecker::checkRelationalOperation( diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index 3a075ecdf..a31299699 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -732,7 +732,8 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool else if (log.get(superTy) || log.get(subTy)) tryUnifyNegations(subTy, superTy); - else if (checkInhabited && !normalizer->isInhabited(subTy)) + // If the normalizer hits resource limits, we can't show it's uninhabited, so, we should error. + else if (checkInhabited && normalizer->isInhabited(subTy) == NormalizationResult::False) { } else @@ -2378,7 +2379,8 @@ void Unifier::tryUnifyScalarShape(TypeId subTy, TypeId superTy, bool reversed) TypeId osubTy = subTy; TypeId osuperTy = superTy; - if (checkInhabited && !normalizer->isInhabited(subTy)) + // If the normalizer hits resource limits, we can't show it's uninhabited, so, we should continue. + if (checkInhabited && normalizer->isInhabited(subTy) == NormalizationResult::False) return; if (reversed) diff --git a/Analysis/src/Unifier2.cpp b/Analysis/src/Unifier2.cpp index 42dabae09..b8f3d225a 100644 --- a/Analysis/src/Unifier2.cpp +++ b/Analysis/src/Unifier2.cpp @@ -584,8 +584,11 @@ struct FreeTypeSearcher : TypeVisitor } } - DenseHashMap negativeTypes{0}; - DenseHashMap positiveTypes{0}; + // The keys in these maps are either TypeIds or TypePackIds. It's safe to + // mix them because we only use these pointers as unique keys. We never + // indirect them. + DenseHashMap negativeTypes{0}; + DenseHashMap positiveTypes{0}; bool visit(TypeId ty) override { @@ -673,6 +676,28 @@ struct FreeTypeSearcher : TypeVisitor { return false; } + + bool visit(TypePackId tp, const FreeTypePack& ftp) override + { + if (!subsumes(scope, ftp.scope)) + return true; + + switch (polarity) + { + case Positive: + positiveTypes[tp]++; + break; + case Negative: + negativeTypes[tp]++; + break; + case Both: + positiveTypes[tp]++; + negativeTypes[tp]++; + break; + } + + return true; + } }; struct MutatingGeneralizer : TypeOnceVisitor @@ -680,15 +705,15 @@ struct MutatingGeneralizer : TypeOnceVisitor NotNull builtinTypes; NotNull scope; - DenseHashMap positiveTypes; - DenseHashMap negativeTypes; + DenseHashMap positiveTypes; + DenseHashMap negativeTypes; std::vector generics; std::vector genericPacks; bool isWithinFunction = false; - MutatingGeneralizer(NotNull builtinTypes, NotNull scope, DenseHashMap positiveTypes, - DenseHashMap negativeTypes) + MutatingGeneralizer(NotNull builtinTypes, NotNull scope, DenseHashMap positiveTypes, + DenseHashMap negativeTypes) : TypeOnceVisitor(/* skipBoundTypes */ true) , builtinTypes(builtinTypes) , scope(scope) @@ -816,7 +841,7 @@ struct MutatingGeneralizer : TypeOnceVisitor return false; } - size_t getCount(const DenseHashMap& map, TypeId ty) + size_t getCount(const DenseHashMap& map, const void* ty) { if (const size_t* count = map.find(ty)) return *count; @@ -849,9 +874,18 @@ struct MutatingGeneralizer : TypeOnceVisitor if (!subsumes(scope, ftp.scope)) return true; - asMutable(tp)->ty.emplace(scope); + tp = follow(tp); + + const size_t positiveCount = getCount(positiveTypes, tp); + const size_t negativeCount = getCount(negativeTypes, tp); - genericPacks.push_back(tp); + if (1 == positiveCount + negativeCount) + asMutable(tp)->ty.emplace(builtinTypes->unknownTypePack); + else + { + asMutable(tp)->ty.emplace(scope); + genericPacks.push_back(tp); + } return true; } diff --git a/CMakeLists.txt b/CMakeLists.txt index 9e99e9313..985cda1c6 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -188,6 +188,12 @@ if(MSVC_IDE) target_sources(Luau.VM PRIVATE tools/natvis/VM.natvis) endif() +# On Windows and Android threads are provided, on Linux/Mac/iOS we use pthreads +add_library(osthreads INTERFACE) +if(CMAKE_SYSTEM_NAME MATCHES "Linux|Darwin|iOS") + target_link_libraries(osthreads INTERFACE "-lpthread") +endif () + if(LUAU_BUILD_CLI) target_compile_options(Luau.Repl.CLI PRIVATE ${LUAU_OPTIONS}) target_compile_options(Luau.Reduce.CLI PRIVATE ${LUAU_OPTIONS}) @@ -200,13 +206,8 @@ if(LUAU_BUILD_CLI) target_link_libraries(Luau.Repl.CLI PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.VM Luau.CLI.lib isocline) - if(UNIX) - find_library(LIBPTHREAD pthread) - if (LIBPTHREAD) - target_link_libraries(Luau.Repl.CLI PRIVATE pthread) - target_link_libraries(Luau.Analyze.CLI PRIVATE pthread) - endif() - endif() + target_link_libraries(Luau.Repl.CLI PRIVATE osthreads) + target_link_libraries(Luau.Analyze.CLI PRIVATE osthreads) target_link_libraries(Luau.Analyze.CLI PRIVATE Luau.Analysis Luau.CLI.lib) @@ -230,18 +231,17 @@ if(LUAU_BUILD_TESTS) target_compile_options(Luau.Conformance PRIVATE ${LUAU_OPTIONS}) target_include_directories(Luau.Conformance PRIVATE extern) target_link_libraries(Luau.Conformance PRIVATE Luau.Analysis Luau.Compiler Luau.CodeGen Luau.VM) - file(REAL_PATH "tests/conformance" LUAU_CONFORMANCE_SOURCE_DIR) + if(CMAKE_SYSTEM_NAME MATCHES "Android|iOS") + set(LUAU_CONFORMANCE_SOURCE_DIR "Client/Luau/tests/conformance") + else () + file(REAL_PATH "tests/conformance" LUAU_CONFORMANCE_SOURCE_DIR) + endif () target_compile_definitions(Luau.Conformance PRIVATE LUAU_CONFORMANCE_SOURCE_DIR="${LUAU_CONFORMANCE_SOURCE_DIR}") target_compile_options(Luau.CLI.Test PRIVATE ${LUAU_OPTIONS}) target_include_directories(Luau.CLI.Test PRIVATE extern CLI) target_link_libraries(Luau.CLI.Test PRIVATE Luau.Compiler Luau.Config Luau.CodeGen Luau.VM Luau.CLI.lib isocline) - if(UNIX) - find_library(LIBPTHREAD pthread) - if (LIBPTHREAD) - target_link_libraries(Luau.CLI.Test PRIVATE pthread) - endif() - endif() + target_link_libraries(Luau.CLI.Test PRIVATE osthreads) endif() diff --git a/CodeGen/include/Luau/NativeProtoExecData.h b/CodeGen/include/Luau/NativeProtoExecData.h index 3c72cc3dc..6beb5d1df 100644 --- a/CodeGen/include/Luau/NativeProtoExecData.h +++ b/CodeGen/include/Luau/NativeProtoExecData.h @@ -21,6 +21,13 @@ struct NativeProtoExecDataHeader // when the NativeProto is bound to the NativeModule via assignToModule(). NativeModule* nativeModule = nullptr; + // We store the native code offset until the code is allocated in executable + // pages, after which point we store the actual address. + const uint8_t* entryOffsetOrAddress = nullptr; + + // The bytecode id of the proto + uint32_t bytecodeId = 0; + // The number of bytecode instructions in the proto. This is the number of // elements in the instruction offsets array following this header. uint32_t bytecodeInstructionCount = 0; diff --git a/CodeGen/include/Luau/SharedCodeAllocator.h b/CodeGen/include/Luau/SharedCodeAllocator.h index 577532e8f..108418933 100644 --- a/CodeGen/include/Luau/SharedCodeAllocator.h +++ b/CodeGen/include/Luau/SharedCodeAllocator.h @@ -28,68 +28,11 @@ namespace CodeGen using ModuleId = std::array; -class NativeProto; +struct CodeAllocator; class NativeModule; class NativeModuleRef; class SharedCodeAllocator; -// A NativeProto represents a single natively-compiled function. A NativeProto -// should be constructed for each function as it is compiled. When compilation -// of all of the functions in a module is complete, the set of NativeProtos -// representing those functions should be passed to the NativeModule constructor. -class NativeProto -{ -public: - NativeProto(uint32_t bytecodeId, NativeProtoExecDataPtr nativeExecData); - - NativeProto(const NativeProto&) = delete; - NativeProto(NativeProto&&) noexcept = default; - NativeProto& operator=(const NativeProto&) = delete; - NativeProto& operator=(NativeProto&&) noexcept = default; - - // This should be called to initialize the NativeProto state prior to - // passing the NativeProto to the NativeModule constructor. - void setEntryOffset(uint32_t entryOffset) noexcept; - - // This will be called by the NativeModule constructor to bind this - // NativeProto to the NativeModule. - void assignToModule(NativeModule* nativeModule) noexcept; - - // Gets the bytecode id for the Proto that was compiled into this NativeProto - [[nodiscard]] uint32_t getBytecodeId() const noexcept; - - // Gets the address of the entry point for this function - [[nodiscard]] const uint8_t* getEntryAddress() const noexcept; - - // Gets the native exec data for this function - [[nodiscard]] const NativeProtoExecDataHeader& getNativeExecDataHeader() const noexcept; - - // The NativeProto stores an array that maps bytecode instruction indices to - // native code offsets relative to the native entry point. When compilation - // and code allocation is complete, we store a pointer to this data in the - // Luau VM Proto object for this function. When we do this, we must acquire - // a reference to the NativeModule that owns this NativeProto. The - // getOwning-version of this function acquires that reference and gets the - // instruction offsets pointer. When the Proto object is destroyed, this - // pointer must be passed to releaseOwningPointerToInstructionOffsets to - // release the reference. - // - // (This structure is designed to make it much more difficult to "forget" - // to acquire a reference.) - [[nodiscard]] const uint32_t* getNonOwningPointerToInstructionOffsets() const noexcept; - [[nodiscard]] const uint32_t* getOwningPointerToInstructionOffsets() const noexcept; - - static void releaseOwningPointerToInstructionOffsets(const uint32_t* ownedInstructionOffsets) noexcept; - -private: - uint32_t bytecodeId = 0; - - // We store the native code offset until assignToModule() is called, after - // which point we store the actual address. - const uint8_t* entryOffsetOrAddress = nullptr; - - NativeProtoExecDataPtr nativeExecData = {}; -}; // A NativeModule represents a single natively-compiled module (script). It is // the unit of shared ownership and is thus where the reference count is @@ -98,8 +41,8 @@ class NativeProto class NativeModule { public: - NativeModule( - SharedCodeAllocator* allocator, const ModuleId& moduleId, const uint8_t* moduleBaseAddress, std::vector nativeProtos) noexcept; + NativeModule(SharedCodeAllocator* allocator, const ModuleId& moduleId, const uint8_t* moduleBaseAddress, + std::vector nativeProtos) noexcept; NativeModule(const NativeModule&) = delete; NativeModule(NativeModule&&) = delete; @@ -112,6 +55,7 @@ class NativeModule ~NativeModule() noexcept; size_t addRef() const noexcept; + size_t addRefs(size_t count) const noexcept; size_t release() const noexcept; [[nodiscard]] size_t getRefcount() const noexcept; @@ -120,7 +64,9 @@ class NativeModule // Attempts to find the NativeProto with the given bytecode id. If no // NativeProto for that bytecode id exists, a null pointer is returned. - [[nodiscard]] const NativeProto* tryGetNativeProto(uint32_t bytecodeId) const noexcept; + [[nodiscard]] const uint32_t* tryGetNativeProto(uint32_t bytecodeId) const noexcept; + + [[nodiscard]] const std::vector& getNativeProtos() const noexcept; private: mutable std::atomic refcount = 0; @@ -129,7 +75,7 @@ class NativeModule ModuleId moduleId = {}; const uint8_t* moduleBaseAddress = nullptr; - std::vector nativeProtos = {}; + std::vector nativeProtos = {}; }; // A NativeModuleRef is an owning reference to a NativeModule. (Note: We do @@ -164,7 +110,7 @@ class NativeModuleRef class SharedCodeAllocator { public: - SharedCodeAllocator() = default; + SharedCodeAllocator(CodeAllocator* codeAllocator) noexcept; SharedCodeAllocator(const SharedCodeAllocator&) = delete; SharedCodeAllocator(SharedCodeAllocator&&) = delete; @@ -180,9 +126,11 @@ class SharedCodeAllocator // If we have a NativeModule for the given ModuleId, an owning reference to // it is returned. Otherwise, a new NativeModule is created for that ModuleId // using the provided NativeProtos, data, and code (space is allocated for the - // data and code such that it can be executed). - NativeModuleRef getOrInsertNativeModule( - const ModuleId& moduleId, std::vector nativeProtos, const std::vector& data, const std::vector& code); + // data and code such that it can be executed). Like std::map::insert, the + // bool result is true if a new module was created; false if an existing + // module is being returned. + std::pair getOrInsertNativeModule(const ModuleId& moduleId, std::vector nativeProtos, + const uint8_t* data, size_t dataSize, const uint8_t* code, size_t codeSize); // If a NativeModule exists for the given ModuleId and that NativeModule // is no longer referenced, the NativeModule is destroyed. This should @@ -200,10 +148,9 @@ class SharedCodeAllocator mutable std::mutex mutex; - // Will be removed when backend allocator is integrated - const uint8_t* baseAddress = reinterpret_cast(0x0f00'0000); - std::unordered_map, ModuleIdHash, std::equal_to<>> nativeModules; + + CodeAllocator* codeAllocator = nullptr; }; } // namespace CodeGen diff --git a/CodeGen/src/CodeGen.cpp b/CodeGen/src/CodeGen.cpp index 24bd07c53..c3c031939 100644 --- a/CodeGen/src/CodeGen.cpp +++ b/CodeGen/src/CodeGen.cpp @@ -67,8 +67,8 @@ namespace CodeGen static const Instruction kCodeEntryInsn = LOP_NATIVECALL; -static void* gPerfLogContext = nullptr; -static PerfLogFn gPerfLogFn = nullptr; +void* gPerfLogContext = nullptr; +PerfLogFn gPerfLogFn = nullptr; struct OldNativeProto { diff --git a/CodeGen/src/CodeGenContext.cpp b/CodeGen/src/CodeGenContext.cpp index 493ca7c2f..26764a892 100644 --- a/CodeGen/src/CodeGenContext.cpp +++ b/CodeGen/src/CodeGenContext.cpp @@ -2,6 +2,7 @@ #include "CodeGenContext.h" #include "CodeGenA64.h" +#include "CodeGenLower.h" #include "CodeGenX64.h" #include "Luau/CodeBlockUnwind.h" @@ -9,7 +10,8 @@ #include "Luau/UnwindBuilderDwarf2.h" #include "Luau/UnwindBuilderWin.h" -LUAU_FASTFLAG(LuauCodegenHeapSizeReport) +#include "lapi.h" + LUAU_FASTINT(LuauCodeGenBlockSize) LUAU_FASTINT(LuauCodeGenMaxTotalSize) @@ -19,10 +21,99 @@ namespace Luau namespace CodeGen { +static const Instruction kCodeEntryInsn = LOP_NATIVECALL; + // From CodeGen.cpp extern void* gPerfLogContext; extern PerfLogFn gPerfLogFn; +unsigned int getCpuFeaturesA64(); + +static void logPerfFunction(Proto* p, uintptr_t addr, unsigned size) +{ + CODEGEN_ASSERT(p->source); + + const char* source = getstr(p->source); + source = (source[0] == '=' || source[0] == '@') ? source + 1 : "[string]"; + + char name[256]; + snprintf(name, sizeof(name), " %s:%d %s", source, p->linedefined, p->debugname ? getstr(p->debugname) : ""); + + if (gPerfLogFn) + gPerfLogFn(gPerfLogContext, addr, size, name); +} + +static void logPerfFunctions( + const std::vector& moduleProtos, const uint8_t* nativeModuleBaseAddress, const std::vector& nativeProtos) +{ + if (gPerfLogFn == nullptr) + return; + + if (nativeProtos.size() > 0) + gPerfLogFn(gPerfLogContext, uintptr_t(nativeModuleBaseAddress), + unsigned(getNativeProtoExecDataHeader(nativeProtos[0].get()).entryOffsetOrAddress - nativeModuleBaseAddress), ""); + + auto protoIt = moduleProtos.begin(); + + for (const NativeProtoExecDataPtr& nativeProto : nativeProtos) + { + const NativeProtoExecDataHeader& header = getNativeProtoExecDataHeader(nativeProto.get()); + + while (protoIt != moduleProtos.end() && uint32_t((**protoIt).bytecodeid) != header.bytecodeId) + { + ++protoIt; + } + + CODEGEN_ASSERT(protoIt != moduleProtos.end()); + + logPerfFunction(*protoIt, uintptr_t(header.entryOffsetOrAddress), uint32_t(header.nativeCodeSize)); + } +} + +// If Release is true, the native proto will be removed from the vector and +// ownership will be assigned to the Proto object (for use with the +// StandaloneCodeContext). If Release is false, the native proto will not be +// removed from the vector (for use with the SharedCodeContext). +template +static size_t bindNativeProtos(const std::vector& moduleProtos, NativeProtosVector& nativeProtos) +{ + size_t protosBound = 0; + + auto protoIt = moduleProtos.begin(); + + for (auto& nativeProto : nativeProtos) + { + const NativeProtoExecDataHeader& header = getNativeProtoExecDataHeader(nativeProto.get()); + + while (protoIt != moduleProtos.end() && uint32_t((**protoIt).bytecodeid) != header.bytecodeId) + { + ++protoIt; + } + + CODEGEN_ASSERT(protoIt != moduleProtos.end()); + + // The NativeProtoExecData is now owned by the VM and will be destroyed + // via onDestroyFunction. + Proto* proto = *protoIt; + + if constexpr (Release) + { + proto->execdata = nativeProto.release(); + } + else + { + proto->execdata = nativeProto.get(); + } + + proto->exectarget = reinterpret_cast(header.entryOffsetOrAddress); + proto->codeentry = &kCodeEntryInsn; + + ++protosBound; + } + + return protosBound; +} + BaseCodeGenContext::BaseCodeGenContext(size_t blockSize, size_t maxTotalSize, AllocationCallback* allocationCallback, void* allocationCallbackContext) : codeAllocator{blockSize, maxTotalSize, allocationCallback, allocationCallbackContext} { @@ -64,7 +155,37 @@ StandaloneCodeGenContext::StandaloneCodeGenContext( { } -void StandaloneCodeGenContext::compileOrBindModule(const ModuleId&, lua_State*, int, unsigned int, CompilationStats*) {} +[[nodiscard]] std::optional StandaloneCodeGenContext::tryBindExistingModule(const ModuleId&, const std::vector&) +{ + // The StandaloneCodeGenContext does not support sharing of native code + return {}; +} + +[[nodiscard]] CodeGenCompilationResult StandaloneCodeGenContext::bindModule(const ModuleId&, const std::vector& moduleProtos, + std::vector nativeProtos, const uint8_t* data, size_t dataSize, const uint8_t* code, size_t codeSize) +{ + uint8_t* nativeData = nullptr; + size_t sizeNativeData = 0; + uint8_t* codeStart = nullptr; + if (!codeAllocator.allocate(data, int(dataSize), code, int(codeSize), nativeData, sizeNativeData, codeStart)) + { + return CodeGenCompilationResult::AllocationFailed; + } + + // Relocate the entry offsets to their final executable addresses: + for (const NativeProtoExecDataPtr& nativeProto : nativeProtos) + { + NativeProtoExecDataHeader& header = getNativeProtoExecDataHeader(nativeProto.get()); + + header.entryOffsetOrAddress = codeStart + reinterpret_cast(header.entryOffsetOrAddress); + } + + logPerfFunctions(moduleProtos, codeStart, nativeProtos); + + bindNativeProtos(moduleProtos, nativeProtos); + + return CodeGenCompilationResult::Success; +} void StandaloneCodeGenContext::onCloseState() noexcept { @@ -82,10 +203,44 @@ void StandaloneCodeGenContext::onDestroyFunction(void* execdata) noexcept SharedCodeGenContext::SharedCodeGenContext( size_t blockSize, size_t maxTotalSize, AllocationCallback* allocationCallback, void* allocationCallbackContext) : BaseCodeGenContext{blockSize, maxTotalSize, allocationCallback, allocationCallbackContext} + , sharedAllocator{&codeAllocator} { } -void SharedCodeGenContext::compileOrBindModule(const ModuleId&, lua_State*, int, unsigned int, CompilationStats*) {} +[[nodiscard]] std::optional SharedCodeGenContext::tryBindExistingModule( + const ModuleId& moduleId, const std::vector& moduleProtos) +{ + NativeModuleRef nativeModule = sharedAllocator.tryGetNativeModule(moduleId); + if (nativeModule.empty()) + { + return {}; + } + + // Bind the native protos and acquire an owning reference for each: + nativeModule->addRefs(bindNativeProtos(moduleProtos, nativeModule->getNativeProtos())); + + return CodeGenCompilationResult::Success; +} + +[[nodiscard]] CodeGenCompilationResult SharedCodeGenContext::bindModule(const ModuleId& moduleId, const std::vector& moduleProtos, + std::vector nativeProtos, const uint8_t* data, size_t dataSize, const uint8_t* code, size_t codeSize) +{ + const std::pair insertionResult = + sharedAllocator.getOrInsertNativeModule(moduleId, std::move(nativeProtos), data, dataSize, code, codeSize); + + // If we did not get a NativeModule back, allocation failed: + if (insertionResult.first.empty()) + return CodeGenCompilationResult::AllocationFailed; + + // If we allocated a new module, log the function code ranges for perf: + if (insertionResult.second) + logPerfFunctions(moduleProtos, insertionResult.first->getModuleBaseAddress(), insertionResult.first->getNativeProtos()); + + // Bind the native protos and acquire an owning reference for each: + insertionResult.first->addRefs(bindNativeProtos(moduleProtos, insertionResult.first->getNativeProtos())); + + return CodeGenCompilationResult::Success; +} void SharedCodeGenContext::onCloseState() noexcept { @@ -165,13 +320,16 @@ static int onEnter(lua_State* L, Proto* proto) return GateFn(codeGenContext->context.gateEntry)(L, proto, target, &codeGenContext->context); } +static int onEnterDisabled(lua_State* L, Proto* proto) +{ + return 1; +} + // Defined in CodeGen.cpp void onDisable(lua_State* L, Proto* proto); static size_t getMemorySize(lua_State* L, Proto* proto) { - CODEGEN_ASSERT(FFlag::LuauCodegenHeapSizeReport); - const NativeProtoExecDataHeader& execDataHeader = getNativeProtoExecDataHeader(static_cast(proto->execdata)); const size_t execDataSize = sizeof(NativeProtoExecDataHeader) + execDataHeader.bytecodeInstructionCount * sizeof(Instruction); @@ -191,9 +349,7 @@ static void initializeExecutionCallbacks(lua_State* L, BaseCodeGenContext* codeG ecb->destroy = onDestroyFunction; ecb->enter = onEnter; ecb->disable = onDisable; - - if (FFlag::LuauCodegenHeapSizeReport) - ecb->getmemorysize = getMemorySize; + ecb->getmemorysize = getMemorySize; } void create_NEW(lua_State* L) @@ -222,5 +378,186 @@ void create_NEW(lua_State* L, SharedCodeGenContext* codeGenContext) initializeExecutionCallbacks(L, codeGenContext); } +[[nodiscard]] static NativeProtoExecDataPtr createNativeProtoExecData(Proto* proto, const IrBuilder& ir) +{ + NativeProtoExecDataPtr nativeExecData = createNativeProtoExecData(proto->sizecode); + + uint32_t instTarget = ir.function.entryLocation; + + for (int i = 0; i < proto->sizecode; ++i) + { + CODEGEN_ASSERT(ir.function.bcMapping[i].asmLocation >= instTarget); + + nativeExecData[i] = ir.function.bcMapping[i].asmLocation - instTarget; + } + + // Set first instruction offset to 0 so that entering this function still + // executes any generated entry code. + nativeExecData[0] = 0; + + NativeProtoExecDataHeader& header = getNativeProtoExecDataHeader(nativeExecData.get()); + header.entryOffsetOrAddress = reinterpret_cast(static_cast(instTarget)); + header.bytecodeId = uint32_t(proto->bytecodeid); + header.bytecodeInstructionCount = proto->sizecode; + + return nativeExecData; +} + +template +[[nodiscard]] static NativeProtoExecDataPtr createNativeFunction( + AssemblyBuilder& build, ModuleHelpers& helpers, Proto* proto, uint32_t& totalIrInstCount, CodeGenCompilationResult& result) +{ + IrBuilder ir; + ir.buildFunctionIr(proto); + + unsigned instCount = unsigned(ir.function.instructions.size()); + + if (totalIrInstCount + instCount >= unsigned(FInt::CodegenHeuristicsInstructionLimit.value)) + { + result = CodeGenCompilationResult::CodeGenOverflowInstructionLimit; + return {}; + } + + totalIrInstCount += instCount; + + if (!lowerFunction(ir, build, helpers, proto, {}, /* stats */ nullptr, result)) + { + return {}; + } + + return createNativeProtoExecData(proto, ir); +} + +CompilationResult compile_NEW(const ModuleId& moduleId, lua_State* L, int idx, unsigned int flags, CompilationStats* stats) +{ + CODEGEN_ASSERT(lua_isLfunction(L, idx)); + const TValue* func = luaA_toobject(L, idx); + + Proto* root = clvalue(func)->l.p; + + if ((flags & CodeGen_OnlyNativeModules) != 0 && (root->flags & LPF_NATIVE_MODULE) == 0) + return CompilationResult{CodeGenCompilationResult::NotNativeModule}; + + BaseCodeGenContext* codeGenContext = getCodeGenContext(L); + if (codeGenContext == nullptr) + return CompilationResult{CodeGenCompilationResult::CodeGenNotInitialized}; + + std::vector protos; + gatherFunctions(protos, root, flags); + + // Skip protos that have been compiled during previous invocations of CodeGen::compile + protos.erase(std::remove_if(protos.begin(), protos.end(), + [](Proto* p) { + return p == nullptr || p->execdata != nullptr; + }), + protos.end()); + + if (protos.empty()) + return CompilationResult{CodeGenCompilationResult::NothingToCompile}; + + if (std::optional existingModuleBindResult = codeGenContext->tryBindExistingModule(moduleId, protos)) + return CompilationResult{*existingModuleBindResult}; + + if (stats != nullptr) + stats->functionsTotal = uint32_t(protos.size()); + +#if defined(__aarch64__) + static unsigned int cpuFeatures = getCpuFeaturesA64(); + A64::AssemblyBuilderA64 build(/* logText= */ false, cpuFeatures); +#else + X64::AssemblyBuilderX64 build(/* logText= */ false); +#endif + + ModuleHelpers helpers; +#if defined(__aarch64__) + A64::assembleHelpers(build, helpers); +#else + X64::assembleHelpers(build, helpers); +#endif + + CompilationResult compilationResult; + + std::vector nativeProtos; + nativeProtos.reserve(protos.size()); + + uint32_t totalIrInstCount = 0; + + for (size_t i = 0; i != protos.size(); ++i) + { + CodeGenCompilationResult protoResult = CodeGenCompilationResult::Success; + + NativeProtoExecDataPtr nativeExecData = createNativeFunction(build, helpers, protos[i], totalIrInstCount, protoResult); + if (nativeExecData != nullptr) + { + nativeProtos.push_back(std::move(nativeExecData)); + } + else + { + compilationResult.protoFailures.push_back( + {protoResult, protos[i]->debugname ? getstr(protos[i]->debugname) : "", protos[i]->linedefined}); + } + } + + // Very large modules might result in overflowing a jump offset; in this + // case we currently abandon the entire module + if (!build.finalize()) + { + compilationResult.result = CodeGenCompilationResult::CodeGenAssemblerFinalizationFailure; + return compilationResult; + } + + // If no functions were assembled, we don't need to allocate/copy executable pages for helpers + if (nativeProtos.empty()) + return compilationResult; + + if (stats != nullptr) + { + for (const NativeProtoExecDataPtr& nativeExecData : nativeProtos) + { + NativeProtoExecDataHeader& header = getNativeProtoExecDataHeader(nativeExecData.get()); + + stats->bytecodeSizeBytes += header.bytecodeInstructionCount * sizeof(Instruction); + + // Account for the native -> bytecode instruction offsets mapping: + stats->nativeMetadataSizeBytes += header.bytecodeInstructionCount * sizeof(uint32_t); + } + + stats->functionsCompiled += uint32_t(nativeProtos.size()); + stats->nativeCodeSizeBytes += build.code.size(); + stats->nativeDataSizeBytes += build.data.size(); + } + + for (size_t i = 0; i < nativeProtos.size(); ++i) + { + NativeProtoExecDataHeader& header = getNativeProtoExecDataHeader(nativeProtos[i].get()); + + uint32_t begin = uint32_t(reinterpret_cast(header.entryOffsetOrAddress)); + uint32_t end = i + 1 < nativeProtos.size() ? uint32_t(uintptr_t(getNativeProtoExecDataHeader(nativeProtos[i + 1].get()).entryOffsetOrAddress)) + : uint32_t(build.code.size()); + + CODEGEN_ASSERT(begin < end); + + header.nativeCodeSize = end - begin; + } + + const CodeGenCompilationResult bindResult = + codeGenContext->bindModule(moduleId, protos, std::move(nativeProtos), reinterpret_cast(build.data.data()), build.data.size(), + reinterpret_cast(build.code.data()), build.code.size()); + if (bindResult != CodeGenCompilationResult::Success) + compilationResult.result = bindResult; + return compilationResult; +} + +[[nodiscard]] bool isNativeExecutionEnabled_NEW(lua_State* L) +{ + return getCodeGenContext(L) != nullptr && L->global->ecb.enter == onEnter; +} + +void setNativeExecutionEnabled_NEW(lua_State* L, bool enabled) +{ + if (getCodeGenContext(L) != nullptr) + L->global->ecb.enter = enabled ? onEnter : onEnterDisabled; +} + } // namespace CodeGen } // namespace Luau diff --git a/CodeGen/src/CodeGenContext.h b/CodeGen/src/CodeGenContext.h index f6ad9ce9f..9e96e3ec3 100644 --- a/CodeGen/src/CodeGenContext.h +++ b/CodeGen/src/CodeGenContext.h @@ -6,6 +6,7 @@ #include "NativeState.h" #include +#include #include namespace Luau @@ -27,7 +28,11 @@ class BaseCodeGenContext [[nodiscard]] bool initHeaderFunctions(); - virtual void compileOrBindModule(const ModuleId& moduleId, lua_State* L, int idx, unsigned int flags, CompilationStats* stats) = 0; + [[nodiscard]] virtual std::optional tryBindExistingModule( + const ModuleId& moduleId, const std::vector& moduleProtos) = 0; + + [[nodiscard]] virtual CodeGenCompilationResult bindModule(const ModuleId& moduleId, const std::vector& moduleProtos, + std::vector nativeExecDatas, const uint8_t* data, size_t dataSize, const uint8_t* code, size_t codeSize) = 0; virtual void onCloseState() noexcept = 0; virtual void onDestroyFunction(void* execdata) noexcept = 0; @@ -46,7 +51,11 @@ class StandaloneCodeGenContext final : public BaseCodeGenContext public: StandaloneCodeGenContext(size_t blockSize, size_t maxTotalSize, AllocationCallback* allocationCallback, void* allocationCallbackContext); - virtual void compileOrBindModule(const ModuleId& moduleId, lua_State* L, int idx, unsigned int flags, CompilationStats* stats) override; + [[nodiscard]] virtual std::optional tryBindExistingModule( + const ModuleId& moduleId, const std::vector& moduleProtos) override; + + [[nodiscard]] virtual CodeGenCompilationResult bindModule(const ModuleId& moduleId, const std::vector& moduleProtos, + std::vector nativeExecDatas, const uint8_t* data, size_t dataSize, const uint8_t* code, size_t codeSize) override; virtual void onCloseState() noexcept override; virtual void onDestroyFunction(void* execdata) noexcept override; @@ -59,7 +68,11 @@ class SharedCodeGenContext final : public BaseCodeGenContext public: SharedCodeGenContext(size_t blockSize, size_t maxTotalSize, AllocationCallback* allocationCallback, void* allocationCallbackContext); - virtual void compileOrBindModule(const ModuleId& moduleId, lua_State* L, int idx, unsigned int flags, CompilationStats* stats) override; + [[nodiscard]] virtual std::optional tryBindExistingModule( + const ModuleId& moduleId, const std::vector& moduleProtos) override; + + [[nodiscard]] virtual CodeGenCompilationResult bindModule(const ModuleId& moduleId, const std::vector& moduleProtos, + std::vector nativeExecDatas, const uint8_t* data, size_t dataSize, const uint8_t* code, size_t codeSize) override; virtual void onCloseState() noexcept override; virtual void onDestroyFunction(void* execdata) noexcept override; @@ -110,5 +123,13 @@ void create_NEW(lua_State* L, size_t blockSize, size_t maxTotalSize, AllocationC // destroyed via lua_close. void create_NEW(lua_State* L, SharedCodeGenContext* codeGenContext); +CompilationResult compile_NEW(const ModuleId& moduleId, lua_State* L, int idx, unsigned int flags, CompilationStats* stats); + +// Returns true if native execution is currently enabled for this VM +[[nodiscard]] bool isNativeExecutionEnabled_NEW(lua_State* L); + +// Enables or disables native excution for this VM +void setNativeExecutionEnabled_NEW(lua_State* L, bool enabled); + } // namespace CodeGen } // namespace Luau diff --git a/CodeGen/src/IrLoweringX64.cpp b/CodeGen/src/IrLoweringX64.cpp index d417f19ff..bd41e6fa4 100644 --- a/CodeGen/src/IrLoweringX64.cpp +++ b/CodeGen/src/IrLoweringX64.cpp @@ -15,8 +15,6 @@ #include "lstate.h" #include "lgc.h" -LUAU_FASTFLAGVARIABLE(LuauCodegenVectorOptAnd, false) -LUAU_FASTFLAGVARIABLE(LuauCodegenSmallerUnm, false) LUAU_FASTFLAGVARIABLE(LuauCodegenCheckTruthyFormB, false) namespace Luau @@ -543,24 +541,7 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) { inst.regX64 = regs.allocRegOrReuse(SizeX64::xmmword, index, {inst.a}); - if (FFlag::LuauCodegenSmallerUnm) - { - build.vxorpd(inst.regX64, regOp(inst.a), build.f64(-0.0)); - } - else - { - RegisterX64 src = regOp(inst.a); - - if (inst.regX64 == src) - { - build.vxorpd(inst.regX64, inst.regX64, build.f64(-0.0)); - } - else - { - build.vmovsd(inst.regX64, src, src); - build.vxorpd(inst.regX64, inst.regX64, build.f64(-0.0)); - } - } + build.vxorpd(inst.regX64, regOp(inst.a), build.f64(-0.0)); break; } case IrCmd::FLOOR_NUM: @@ -611,128 +592,59 @@ void IrLoweringX64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) { inst.regX64 = regs.allocRegOrReuse(SizeX64::xmmword, index, {inst.a, inst.b}); - if (FFlag::LuauCodegenVectorOptAnd) - { - ScopedRegX64 tmp1{regs}; - ScopedRegX64 tmp2{regs}; + ScopedRegX64 tmp1{regs}; + ScopedRegX64 tmp2{regs}; - RegisterX64 tmpa = vecOp(inst.a, tmp1); - RegisterX64 tmpb = (inst.a == inst.b) ? tmpa : vecOp(inst.b, tmp2); + RegisterX64 tmpa = vecOp(inst.a, tmp1); + RegisterX64 tmpb = (inst.a == inst.b) ? tmpa : vecOp(inst.b, tmp2); - build.vaddps(inst.regX64, tmpa, tmpb); - } - else - { - ScopedRegX64 tmp1{regs, SizeX64::xmmword}; - ScopedRegX64 tmp2{regs, SizeX64::xmmword}; - - // Fourth component is the tag number which is interpreted as a denormal and has to be filtered out - build.vandps(tmp1.reg, regOp(inst.a), vectorAndMaskOp()); - build.vandps(tmp2.reg, regOp(inst.b), vectorAndMaskOp()); - build.vaddps(inst.regX64, tmp1.reg, tmp2.reg); - } + build.vaddps(inst.regX64, tmpa, tmpb); break; } case IrCmd::SUB_VEC: { inst.regX64 = regs.allocRegOrReuse(SizeX64::xmmword, index, {inst.a, inst.b}); - if (FFlag::LuauCodegenVectorOptAnd) - { - ScopedRegX64 tmp1{regs}; - ScopedRegX64 tmp2{regs}; - - RegisterX64 tmpa = vecOp(inst.a, tmp1); - RegisterX64 tmpb = (inst.a == inst.b) ? tmpa : vecOp(inst.b, tmp2); + ScopedRegX64 tmp1{regs}; + ScopedRegX64 tmp2{regs}; - build.vsubps(inst.regX64, tmpa, tmpb); - } - else - { - ScopedRegX64 tmp1{regs, SizeX64::xmmword}; - ScopedRegX64 tmp2{regs, SizeX64::xmmword}; + RegisterX64 tmpa = vecOp(inst.a, tmp1); + RegisterX64 tmpb = (inst.a == inst.b) ? tmpa : vecOp(inst.b, tmp2); - // Fourth component is the tag number which is interpreted as a denormal and has to be filtered out - build.vandps(tmp1.reg, regOp(inst.a), vectorAndMaskOp()); - build.vandps(tmp2.reg, regOp(inst.b), vectorAndMaskOp()); - build.vsubps(inst.regX64, tmp1.reg, tmp2.reg); - } + build.vsubps(inst.regX64, tmpa, tmpb); break; } case IrCmd::MUL_VEC: { inst.regX64 = regs.allocRegOrReuse(SizeX64::xmmword, index, {inst.a, inst.b}); - if (FFlag::LuauCodegenVectorOptAnd) - { - ScopedRegX64 tmp1{regs}; - ScopedRegX64 tmp2{regs}; - - RegisterX64 tmpa = vecOp(inst.a, tmp1); - RegisterX64 tmpb = (inst.a == inst.b) ? tmpa : vecOp(inst.b, tmp2); + ScopedRegX64 tmp1{regs}; + ScopedRegX64 tmp2{regs}; - build.vmulps(inst.regX64, tmpa, tmpb); - } - else - { - ScopedRegX64 tmp1{regs, SizeX64::xmmword}; - ScopedRegX64 tmp2{regs, SizeX64::xmmword}; + RegisterX64 tmpa = vecOp(inst.a, tmp1); + RegisterX64 tmpb = (inst.a == inst.b) ? tmpa : vecOp(inst.b, tmp2); - // Fourth component is the tag number which is interpreted as a denormal and has to be filtered out - build.vandps(tmp1.reg, regOp(inst.a), vectorAndMaskOp()); - build.vandps(tmp2.reg, regOp(inst.b), vectorAndMaskOp()); - build.vmulps(inst.regX64, tmp1.reg, tmp2.reg); - } + build.vmulps(inst.regX64, tmpa, tmpb); break; } case IrCmd::DIV_VEC: { inst.regX64 = regs.allocRegOrReuse(SizeX64::xmmword, index, {inst.a, inst.b}); - if (FFlag::LuauCodegenVectorOptAnd) - { - ScopedRegX64 tmp1{regs}; - ScopedRegX64 tmp2{regs}; + ScopedRegX64 tmp1{regs}; + ScopedRegX64 tmp2{regs}; - RegisterX64 tmpa = vecOp(inst.a, tmp1); - RegisterX64 tmpb = (inst.a == inst.b) ? tmpa : vecOp(inst.b, tmp2); + RegisterX64 tmpa = vecOp(inst.a, tmp1); + RegisterX64 tmpb = (inst.a == inst.b) ? tmpa : vecOp(inst.b, tmp2); - build.vdivps(inst.regX64, tmpa, tmpb); - } - else - { - ScopedRegX64 tmp1{regs, SizeX64::xmmword}; - ScopedRegX64 tmp2{regs, SizeX64::xmmword}; - - // Fourth component is the tag number which is interpreted as a denormal and has to be filtered out - build.vandps(tmp1.reg, regOp(inst.a), vectorAndMaskOp()); - build.vandps(tmp2.reg, regOp(inst.b), vectorAndMaskOp()); - build.vdivps(inst.regX64, tmp1.reg, tmp2.reg); - } + build.vdivps(inst.regX64, tmpa, tmpb); break; } case IrCmd::UNM_VEC: { inst.regX64 = regs.allocRegOrReuse(SizeX64::xmmword, index, {inst.a}); - if (FFlag::LuauCodegenSmallerUnm) - { - build.vxorpd(inst.regX64, regOp(inst.a), build.f32x4(-0.0, -0.0, -0.0, -0.0)); - } - else - { - RegisterX64 src = regOp(inst.a); - - if (inst.regX64 == src) - { - build.vxorpd(inst.regX64, inst.regX64, build.f32x4(-0.0, -0.0, -0.0, -0.0)); - } - else - { - build.vmovsd(inst.regX64, src, src); - build.vxorpd(inst.regX64, inst.regX64, build.f32x4(-0.0, -0.0, -0.0, -0.0)); - } - } + build.vxorpd(inst.regX64, regOp(inst.a), build.f32x4(-0.0, -0.0, -0.0, -0.0)); break; } case IrCmd::NOT_ANY: @@ -2293,17 +2205,15 @@ OperandX64 IrLoweringX64::bufferAddrOp(IrOp bufferOp, IrOp indexOp) RegisterX64 IrLoweringX64::vecOp(IrOp op, ScopedRegX64& tmp) { - if (FFlag::LuauCodegenVectorOptAnd) - { - IrInst source = function.instOp(op); - CODEGEN_ASSERT(source.cmd != IrCmd::SUBSTITUTE); // we don't process substitutions + IrInst source = function.instOp(op); + CODEGEN_ASSERT(source.cmd != IrCmd::SUBSTITUTE); // we don't process substitutions + + // source that comes from memory or from tag instruction has .w = TVECTOR, which is denormal + // to avoid performance degradation on some CPUs we mask this component to produce zero + // otherwise we conservatively assume the vector is a result of a well formed math op so .w is a normal number or zero + if (source.cmd != IrCmd::LOAD_TVALUE && source.cmd != IrCmd::TAG_VECTOR) + return regOp(op); - // source that comes from memory or from tag instruction has .w = TVECTOR, which is denormal - // to avoid performance degradation on some CPUs we mask this component to produce zero - // otherwise we conservatively assume the vector is a result of a well formed math op so .w is a normal number or zero - if (source.cmd != IrCmd::LOAD_TVALUE && source.cmd != IrCmd::TAG_VECTOR) - return regOp(op); - } tmp.alloc(SizeX64::xmmword); build.vandps(tmp.reg, regOp(op), vectorAndMaskOp()); return tmp.reg; diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index 01e81d0f6..974873da7 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -17,7 +17,6 @@ LUAU_FASTINTVARIABLE(LuauCodeGenMinLinearBlockPath, 3) LUAU_FASTINTVARIABLE(LuauCodeGenReuseSlotLimit, 64) LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false) -LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauCodeGenCoverForgprepEffect, false) LUAU_FASTFLAG(LuauCodegenRemoveDeadStores4) LUAU_FASTFLAG(LuauCodegenLoadTVTag) LUAU_FASTFLAGVARIABLE(LuauCodegenInferNumTag, false) @@ -1462,9 +1461,7 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& state.invalidate(IrOp{inst.b.kind, vmRegOp(inst.b) + 0u}); state.invalidate(IrOp{inst.b.kind, vmRegOp(inst.b) + 1u}); state.invalidate(IrOp{inst.b.kind, vmRegOp(inst.b) + 2u}); - - if (DFFlag::LuauCodeGenCoverForgprepEffect) - state.invalidateUserCall(); + state.invalidateUserCall(); break; } } diff --git a/CodeGen/src/SharedCodeAllocator.cpp b/CodeGen/src/SharedCodeAllocator.cpp index b26bd245f..5ca5d8c52 100644 --- a/CodeGen/src/SharedCodeAllocator.cpp +++ b/CodeGen/src/SharedCodeAllocator.cpp @@ -1,6 +1,8 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/SharedCodeAllocator.h" +#include "Luau/CodeAllocator.h" + #include #include #include @@ -11,85 +13,34 @@ namespace CodeGen { - -NativeProto::NativeProto(uint32_t bytecodeId, NativeProtoExecDataPtr nativeExecData) - : bytecodeId{bytecodeId} - , nativeExecData{std::move(nativeExecData)} -{ -} - -void NativeProto::setEntryOffset(uint32_t entryOffset) noexcept -{ - entryOffsetOrAddress = reinterpret_cast(static_cast(entryOffset)); -} - -void NativeProto::assignToModule(NativeModule* nativeModule) noexcept -{ - getNativeProtoExecDataHeader(nativeExecData.get()).nativeModule = nativeModule; - - entryOffsetOrAddress = nativeModule->getModuleBaseAddress() + reinterpret_cast(entryOffsetOrAddress); -} - -[[nodiscard]] uint32_t NativeProto::getBytecodeId() const noexcept -{ - return bytecodeId; -} - -[[nodiscard]] const uint8_t* NativeProto::getEntryAddress() const noexcept -{ - return entryOffsetOrAddress; -} - -[[nodiscard]] const NativeProtoExecDataHeader& NativeProto::getNativeExecDataHeader() const noexcept -{ - return getNativeProtoExecDataHeader(nativeExecData.get()); -} - -[[nodiscard]] const uint32_t* NativeProto::getNonOwningPointerToInstructionOffsets() const noexcept -{ - return nativeExecData.get(); -} - -[[nodiscard]] const uint32_t* NativeProto::getOwningPointerToInstructionOffsets() const noexcept -{ - getNativeProtoExecDataHeader(nativeExecData.get()).nativeModule->addRef(); - return nativeExecData.get(); -} - -void NativeProto::releaseOwningPointerToInstructionOffsets(const uint32_t* ownedInstructionOffsets) noexcept -{ - getNativeProtoExecDataHeader(ownedInstructionOffsets).nativeModule->release(); -} - - struct NativeProtoBytecodeIdEqual { - [[nodiscard]] bool operator()(const NativeProto& left, const NativeProto& right) const noexcept + [[nodiscard]] bool operator()(const NativeProtoExecDataPtr& left, const NativeProtoExecDataPtr& right) const noexcept { - return left.getBytecodeId() == right.getBytecodeId(); + return getNativeProtoExecDataHeader(left.get()).bytecodeId == getNativeProtoExecDataHeader(right.get()).bytecodeId; } }; struct NativeProtoBytecodeIdLess { - [[nodiscard]] bool operator()(const NativeProto& left, const NativeProto& right) const noexcept + [[nodiscard]] bool operator()(const NativeProtoExecDataPtr& left, const NativeProtoExecDataPtr& right) const noexcept { - return left.getBytecodeId() < right.getBytecodeId(); + return getNativeProtoExecDataHeader(left.get()).bytecodeId < getNativeProtoExecDataHeader(right.get()).bytecodeId; } - [[nodiscard]] bool operator()(const NativeProto& left, uint32_t right) const noexcept + [[nodiscard]] bool operator()(const NativeProtoExecDataPtr& left, uint32_t right) const noexcept { - return left.getBytecodeId() < right; + return getNativeProtoExecDataHeader(left.get()).bytecodeId < right; } - [[nodiscard]] bool operator()(uint32_t left, const NativeProto& right) const noexcept + [[nodiscard]] bool operator()(uint32_t left, const NativeProtoExecDataPtr& right) const noexcept { - return left < right.getBytecodeId(); + return left < getNativeProtoExecDataHeader(right.get()).bytecodeId; } }; -NativeModule::NativeModule( - SharedCodeAllocator* allocator, const ModuleId& moduleId, const uint8_t* moduleBaseAddress, std::vector nativeProtos) noexcept +NativeModule::NativeModule(SharedCodeAllocator* allocator, const ModuleId& moduleId, const uint8_t* moduleBaseAddress, + std::vector nativeProtos) noexcept : allocator{allocator} , moduleId{moduleId} , moduleBaseAddress{moduleBaseAddress} @@ -99,9 +50,11 @@ NativeModule::NativeModule( LUAU_ASSERT(moduleBaseAddress != nullptr); // Bind all of the NativeProtos to this module: - for (NativeProto& nativeProto : this->nativeProtos) + for (const NativeProtoExecDataPtr& nativeProto : this->nativeProtos) { - nativeProto.assignToModule(this); + NativeProtoExecDataHeader& header = getNativeProtoExecDataHeader(nativeProto.get()); + header.nativeModule = this; + header.entryOffsetOrAddress = moduleBaseAddress + reinterpret_cast(header.entryOffsetOrAddress); } std::sort(this->nativeProtos.begin(), this->nativeProtos.end(), NativeProtoBytecodeIdLess{}); @@ -120,6 +73,11 @@ size_t NativeModule::addRef() const noexcept return refcount.fetch_add(1) + 1; } +size_t NativeModule::addRefs(size_t count) const noexcept +{ + return refcount.fetch_add(count) + count; +} + size_t NativeModule::release() const noexcept { size_t newRefcount = refcount.fetch_sub(1) - 1; @@ -143,7 +101,7 @@ size_t NativeModule::release() const noexcept return moduleBaseAddress; } -[[nodiscard]] const NativeProto* NativeModule::tryGetNativeProto(uint32_t bytecodeId) const noexcept +[[nodiscard]] const uint32_t* NativeModule::tryGetNativeProto(uint32_t bytecodeId) const noexcept { const auto range = std::equal_range(nativeProtos.begin(), nativeProtos.end(), bytecodeId, NativeProtoBytecodeIdLess{}); if (range.first == range.second) @@ -151,7 +109,12 @@ size_t NativeModule::release() const noexcept LUAU_ASSERT(std::next(range.first) == range.second); - return &*range.first; + return range.first->get(); +} + +[[nodiscard]] const std::vector& NativeModule::getNativeProtos() const noexcept +{ + return nativeProtos; } @@ -226,6 +189,11 @@ NativeModuleRef::operator bool() const noexcept } +SharedCodeAllocator::SharedCodeAllocator(CodeAllocator* codeAllocator) noexcept + : codeAllocator{codeAllocator} +{ +} + SharedCodeAllocator::~SharedCodeAllocator() noexcept { // The allocator should not be destroyed until all outstanding references @@ -240,22 +208,26 @@ SharedCodeAllocator::~SharedCodeAllocator() noexcept return tryGetNativeModuleWithLockHeld(moduleId); } -NativeModuleRef SharedCodeAllocator::getOrInsertNativeModule( - const ModuleId& moduleId, std::vector nativeProtos, const std::vector& data, const std::vector& code) +std::pair SharedCodeAllocator::getOrInsertNativeModule(const ModuleId& moduleId, + std::vector nativeProtos, const uint8_t* data, size_t dataSize, const uint8_t* code, size_t codeSize) { std::unique_lock lock{mutex}; if (NativeModuleRef existingModule = tryGetNativeModuleWithLockHeld(moduleId)) - return existingModule; + return {std::move(existingModule), false}; - // We simulate allocation until the backend allocator is integrated + uint8_t* nativeData = nullptr; + size_t sizeNativeData = 0; + uint8_t* codeStart = nullptr; + if (!codeAllocator->allocate(data, int(dataSize), code, int(codeSize), nativeData, sizeNativeData, codeStart)) + { + return {}; + } std::unique_ptr& nativeModule = nativeModules[moduleId]; - nativeModule = std::make_unique(this, moduleId, baseAddress, std::move(nativeProtos)); - - baseAddress += data.size() + code.size(); + nativeModule = std::make_unique(this, moduleId, codeStart, std::move(nativeProtos)); - return NativeModuleRef{nativeModule.get()}; + return {NativeModuleRef{nativeModule.get()}, true}; } void SharedCodeAllocator::eraseNativeModuleIfUnreferenced(const ModuleId& moduleId) diff --git a/VM/src/lfunc.cpp b/VM/src/lfunc.cpp index 362d01251..5f7c94335 100644 --- a/VM/src/lfunc.cpp +++ b/VM/src/lfunc.cpp @@ -6,85 +6,50 @@ #include "lmem.h" #include "lgc.h" -LUAU_FASTFLAGVARIABLE(LuauNewProtoInitAll, false) - Proto* luaF_newproto(lua_State* L) { Proto* f = luaM_newgco(L, Proto, sizeof(Proto), L->activememcat); luaC_init(L, f, LUA_TPROTO); - if (FFlag::LuauNewProtoInitAll) - { - f->nups = 0; - f->numparams = 0; - f->is_vararg = 0; - f->maxstacksize = 0; - f->flags = 0; - - f->k = NULL; - f->code = NULL; - f->p = NULL; - f->codeentry = NULL; - - f->execdata = NULL; - f->exectarget = 0; - - f->lineinfo = NULL; - f->abslineinfo = NULL; - f->locvars = NULL; - f->upvalues = NULL; - f->source = NULL; - - f->debugname = NULL; - f->debuginsn = NULL; - - f->typeinfo = NULL; - - f->userdata = NULL; - - f->gclist = NULL; - - f->sizecode = 0; - f->sizep = 0; - f->sizelocvars = 0; - f->sizeupvalues = 0; - f->sizek = 0; - f->sizelineinfo = 0; - f->linegaplog2 = 0; - f->linedefined = 0; - f->bytecodeid = 0; - } - else - { - f->k = NULL; - f->sizek = 0; - f->p = NULL; - f->sizep = 0; - f->code = NULL; - f->sizecode = 0; - f->sizeupvalues = 0; - f->nups = 0; - f->upvalues = NULL; - f->numparams = 0; - f->is_vararg = 0; - f->maxstacksize = 0; - f->flags = 0; - f->sizelineinfo = 0; - f->linegaplog2 = 0; - f->lineinfo = NULL; - f->abslineinfo = NULL; - f->sizelocvars = 0; - f->locvars = NULL; - f->source = NULL; - f->debugname = NULL; - f->debuginsn = NULL; - f->codeentry = NULL; - f->execdata = NULL; - f->exectarget = 0; - f->typeinfo = NULL; - f->userdata = NULL; - } + f->nups = 0; + f->numparams = 0; + f->is_vararg = 0; + f->maxstacksize = 0; + f->flags = 0; + + f->k = NULL; + f->code = NULL; + f->p = NULL; + f->codeentry = NULL; + + f->execdata = NULL; + f->exectarget = 0; + + f->lineinfo = NULL; + f->abslineinfo = NULL; + f->locvars = NULL; + f->upvalues = NULL; + f->source = NULL; + + f->debugname = NULL; + f->debuginsn = NULL; + + f->typeinfo = NULL; + + f->userdata = NULL; + + f->gclist = NULL; + + f->sizecode = 0; + f->sizep = 0; + f->sizelocvars = 0; + f->sizeupvalues = 0; + f->sizek = 0; + f->sizelineinfo = 0; + f->linegaplog2 = 0; + f->linedefined = 0; + f->bytecodeid = 0; return f; } diff --git a/VM/src/lvmload.cpp b/VM/src/lvmload.cpp index 2f73d8bc0..681d05236 100644 --- a/VM/src/lvmload.cpp +++ b/VM/src/lvmload.cpp @@ -13,8 +13,6 @@ #include -LUAU_FASTFLAGVARIABLE(LuauLoadExceptionSafe, false) - // TODO: RAII deallocation doesn't work for longjmp builds if a memory error happens template struct TempBuffer @@ -54,11 +52,8 @@ struct ScopedSetGCThreshold ScopedSetGCThreshold(global_State* global, size_t newThreshold) noexcept : global{global} { - if (FFlag::LuauLoadExceptionSafe) - { - originalThreshold = global->GCthreshold; - global->GCthreshold = newThreshold; - } + originalThreshold = global->GCthreshold; + global->GCthreshold = newThreshold; } ScopedSetGCThreshold(const ScopedSetGCThreshold&) = delete; @@ -69,10 +64,7 @@ struct ScopedSetGCThreshold ~ScopedSetGCThreshold() noexcept { - if (FFlag::LuauLoadExceptionSafe) - { - global->GCthreshold = originalThreshold; - } + global->GCthreshold = originalThreshold; } private: @@ -222,12 +214,6 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size // pause GC for the duration of deserialization - some objects we're creating aren't rooted const ScopedSetGCThreshold pauseGC{L->global, SIZE_MAX}; - size_t GCthreshold = L->global->GCthreshold; - - if (!FFlag::LuauLoadExceptionSafe) - { - L->global->GCthreshold = SIZE_MAX; - } // env is 0 for current environment and a stack index otherwise Table* envt = (env == 0) ? L->gt : hvalue(luaA_toobject(L, env)); @@ -289,44 +275,25 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size offset += typesize; } - if (FFlag::LuauLoadExceptionSafe) - { - const int sizecode = readVarInt(data, size, offset); - p->code = luaM_newarray(L, sizecode, Instruction, p->memcat); - p->sizecode = sizecode; - } - else - { - p->sizecode = readVarInt(data, size, offset); - p->code = luaM_newarray(L, p->sizecode, Instruction, p->memcat); - } + const int sizecode = readVarInt(data, size, offset); + p->code = luaM_newarray(L, sizecode, Instruction, p->memcat); + p->sizecode = sizecode; for (int j = 0; j < p->sizecode; ++j) p->code[j] = read(data, size, offset); p->codeentry = p->code; - if (FFlag::LuauLoadExceptionSafe) - { - const int sizek = readVarInt(data, size, offset); - p->k = luaM_newarray(L, sizek, TValue, p->memcat); - p->sizek = sizek; - } - else - { - p->sizek = readVarInt(data, size, offset); - p->k = luaM_newarray(L, p->sizek, TValue, p->memcat); - } + const int sizek = readVarInt(data, size, offset); + p->k = luaM_newarray(L, sizek, TValue, p->memcat); + p->sizek = sizek; - if (FFlag::LuauLoadExceptionSafe) + // Initialize the constants to nil to ensure they have a valid state + // in the event that some operation in the following loop fails with + // an exception. + for (int j = 0; j < p->sizek; ++j) { - // Initialize the constants to nil to ensure they have a valid state - // in the event that some operation in the following loop fails with - // an exception. - for (int j = 0; j < p->sizek; ++j) - { - setnilvalue(&p->k[j]); - } + setnilvalue(&p->k[j]); } for (int j = 0; j < p->sizek; ++j) @@ -334,10 +301,7 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size switch (read(data, size, offset)) { case LBC_CONSTANT_NIL: - if (!FFlag::LuauLoadExceptionSafe) - { - setnilvalue(&p->k[j]); - } + // All constants have already been pre-initialized to nil break; case LBC_CONSTANT_BOOLEAN: @@ -409,17 +373,9 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size } } - if (FFlag::LuauLoadExceptionSafe) - { - const int sizep = readVarInt(data, size, offset); - p->p = luaM_newarray(L, sizep, Proto*, p->memcat); - p->sizep = sizep; - } - else - { - p->sizep = readVarInt(data, size, offset); - p->p = luaM_newarray(L, p->sizep, Proto*, p->memcat); - } + const int sizep = readVarInt(data, size, offset); + p->p = luaM_newarray(L, sizep, Proto*, p->memcat); + p->sizep = sizep; for (int j = 0; j < p->sizep; ++j) { @@ -439,17 +395,9 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size int intervals = ((p->sizecode - 1) >> p->linegaplog2) + 1; int absoffset = (p->sizecode + 3) & ~3; - if (FFlag::LuauLoadExceptionSafe) - { - const int sizelineinfo = absoffset + intervals * sizeof(int); - p->lineinfo = luaM_newarray(L, sizelineinfo, uint8_t, p->memcat); - p->sizelineinfo = sizelineinfo; - } - else - { - p->sizelineinfo = absoffset + intervals * sizeof(int); - p->lineinfo = luaM_newarray(L, p->sizelineinfo, uint8_t, p->memcat); - } + const int sizelineinfo = absoffset + intervals * sizeof(int); + p->lineinfo = luaM_newarray(L, sizelineinfo, uint8_t, p->memcat); + p->sizelineinfo = sizelineinfo; p->abslineinfo = (int*)(p->lineinfo + absoffset); @@ -472,17 +420,9 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size if (debuginfo) { - if (FFlag::LuauLoadExceptionSafe) - { - const int sizelocvars = readVarInt(data, size, offset); - p->locvars = luaM_newarray(L, sizelocvars, LocVar, p->memcat); - p->sizelocvars = sizelocvars; - } - else - { - p->sizelocvars = readVarInt(data, size, offset); - p->locvars = luaM_newarray(L, p->sizelocvars, LocVar, p->memcat); - } + const int sizelocvars = readVarInt(data, size, offset); + p->locvars = luaM_newarray(L, sizelocvars, LocVar, p->memcat); + p->sizelocvars = sizelocvars; for (int j = 0; j < p->sizelocvars; ++j) { @@ -492,17 +432,9 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size p->locvars[j].reg = read(data, size, offset); } - if (FFlag::LuauLoadExceptionSafe) - { - const int sizeupvalues = readVarInt(data, size, offset); - p->upvalues = luaM_newarray(L, sizeupvalues, TString*, p->memcat); - p->sizeupvalues = sizeupvalues; - } - else - { - p->sizeupvalues = readVarInt(data, size, offset); - p->upvalues = luaM_newarray(L, p->sizeupvalues, TString*, p->memcat); - } + const int sizeupvalues = readVarInt(data, size, offset); + p->upvalues = luaM_newarray(L, sizeupvalues, TString*, p->memcat); + p->sizeupvalues = sizeupvalues; for (int j = 0; j < p->sizeupvalues; ++j) { @@ -523,10 +455,5 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size setclvalue(L, L->top, cl); incr_top(L); - if (!FFlag::LuauLoadExceptionSafe) - { - L->global->GCthreshold = GCthreshold; - } - return 0; } diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 8d57cb506..f86b4000e 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -15,7 +15,6 @@ LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2) LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) -LUAU_FASTFLAG(LuauAutocompleteStringLiteralBounds); using namespace Luau; @@ -3188,7 +3187,6 @@ TEST_CASE_FIXTURE(ACFixture, "string_singleton_as_table_key") TEST_CASE_FIXTURE(ACFixture, "string_singleton_in_if_statement") { ScopedFastFlag sff[]{ - {FFlag::LuauAutocompleteStringLiteralBounds, true}, {FFlag::DebugLuauDeferredConstraintResolution, true}, }; @@ -3215,7 +3213,93 @@ TEST_CASE_FIXTURE(ACFixture, "string_singleton_in_if_statement") ac = autocomplete('2'); - CHECK(ac.entryMap.count("left")); + LUAU_CHECK_HAS_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_KEY(ac.entryMap, "right"); + + ac = autocomplete('3'); + + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "right"); + + ac = autocomplete('4'); + + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "right"); + + ac = autocomplete('5'); + + LUAU_CHECK_HAS_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_KEY(ac.entryMap, "right"); + + ac = autocomplete('6'); + + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "right"); + + ac = autocomplete('7'); + + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "right"); + + ac = autocomplete('8'); + + LUAU_CHECK_HAS_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_KEY(ac.entryMap, "right"); + + ac = autocomplete('9'); + + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "right"); + + ac = autocomplete('A'); + + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "right"); + + ac = autocomplete('B'); + + LUAU_CHECK_HAS_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_KEY(ac.entryMap, "right"); + + ac = autocomplete('C'); + + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "right"); +} + +// https://github.com/Roblox/luau/issues/858 +TEST_CASE_FIXTURE(ACFixture, "string_singleton_in_if_statement2") +{ + ScopedFastFlag sff[]{ + {FFlag::DebugLuauDeferredConstraintResolution, true}, + }; + + check(R"( + --!strict + + type Direction = "left" | "right" + + local dir: Direction + -- typestate here means dir is actually typed as `"left"` + dir = "left" + + if dir == @1"@2"@3 then end + local a: {[Direction]: boolean} = {[@4"@5"@6]} + + if dir == @7`@8`@9 then end + local a: {[Direction]: boolean} = {[@A`@B`@C]} + )"); + + Luau::AutocompleteResult ac; + + ac = autocomplete('1'); + + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "right"); + + ac = autocomplete('2'); + + LUAU_CHECK_HAS_KEY(ac.entryMap, "left"); LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "right"); ac = autocomplete('3'); @@ -3231,7 +3315,7 @@ TEST_CASE_FIXTURE(ACFixture, "string_singleton_in_if_statement") ac = autocomplete('5'); LUAU_CHECK_HAS_KEY(ac.entryMap, "left"); - CHECK(ac.entryMap.count("right")); + LUAU_CHECK_HAS_KEY(ac.entryMap, "right"); ac = autocomplete('6'); @@ -3245,7 +3329,7 @@ TEST_CASE_FIXTURE(ACFixture, "string_singleton_in_if_statement") ac = autocomplete('8'); - CHECK(ac.entryMap.count("left")); + LUAU_CHECK_HAS_KEY(ac.entryMap, "left"); LUAU_CHECK_HAS_NO_KEY(ac.entryMap, "right"); ac = autocomplete('9'); @@ -3260,8 +3344,8 @@ TEST_CASE_FIXTURE(ACFixture, "string_singleton_in_if_statement") ac = autocomplete('B'); - CHECK(ac.entryMap.count("left")); - CHECK(ac.entryMap.count("right")); + LUAU_CHECK_HAS_KEY(ac.entryMap, "left"); + LUAU_CHECK_HAS_KEY(ac.entryMap, "right"); ac = autocomplete('C'); diff --git a/tests/ClassFixture.cpp b/tests/ClassFixture.cpp index 980df7117..05df44e1d 100644 --- a/tests/ClassFixture.cpp +++ b/tests/ClassFixture.cpp @@ -22,10 +22,10 @@ ClassFixture::ClassFixture() TypeId baseClassInstanceType = arena.addType(ClassType{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test"}); getMutable(baseClassInstanceType)->props = { - {"BaseMethod", {makeFunction(arena, baseClassInstanceType, {numberType}, {})}}, + {"BaseMethod", Property::readonly(makeFunction(arena, baseClassInstanceType, {numberType}, {}))}, {"BaseField", {numberType}}, - {"Touched", {connectionType}}, + {"Touched", Property::readonly(connectionType)}, }; getMutable(connectionType)->props = { diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index 68904e371..1ab49c821 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -32,7 +32,6 @@ void luaC_validate(lua_State* L); LUAU_FASTFLAG(DebugLuauAbortingChecks) LUAU_FASTINT(CodegenHeuristicsInstructionLimit) -LUAU_FASTFLAG(LuauLoadExceptionSafe) LUAU_DYNAMIC_FASTFLAG(LuauDebugInfoDupArgLeftovers) LUAU_FASTFLAG(LuauCompileRepeatUntilSkippedLocals) LUAU_FASTFLAG(LuauCodegenInferNumTag) @@ -2171,8 +2170,6 @@ TEST_CASE("HugeFunctionLoadFailure") // luau_load. This should require two "large" allocations: One for the // code array and one for the constants array (k). We run this test twice // and fail each of these two allocations. - ScopedFastFlag luauLoadExceptionSafe{FFlag::LuauLoadExceptionSafe, true}; - std::string source = makeHugeFunctionSource(); static const size_t expectedTotalLargeAllocations = 2; diff --git a/tests/NonStrictTypeChecker.test.cpp b/tests/NonStrictTypeChecker.test.cpp index a98c8dbd9..3ffe6dc34 100644 --- a/tests/NonStrictTypeChecker.test.cpp +++ b/tests/NonStrictTypeChecker.test.cpp @@ -92,6 +92,20 @@ declare foo: { declare function @checked optionalArgsAtTheEnd1(x: string, y: number?, z: number?) : number declare function @checked optionalArgsAtTheEnd2(x: string, y: number?, z: string) : number + +type DateTypeArg = { + year: number, + month: number, + day: number, + hour: number?, + min: number?, + sec: number?, + isdst: boolean?, +} + +declare os : { + time: @checked (time: DateTypeArg?) -> number +} )BUILTIN_SRC"; }; @@ -505,4 +519,12 @@ optionalArgsAtTheEnd2("a", "b", "c") -- error CHECK_EQ(2, r1->actual); } +TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "non_testable_type_throws_ice") +{ + CHECK_THROWS_AS(checkNonStrict(R"( +os.time({year = 0, month = 0, day = 0, min = 0, isdst = nil}) +)"), + Luau::InternalCompilerError); +} + TEST_SUITE_END(); diff --git a/tests/Normalize.test.cpp b/tests/Normalize.test.cpp index 81cb7cee2..97d2dafe9 100644 --- a/tests/Normalize.test.cpp +++ b/tests/Normalize.test.cpp @@ -731,7 +731,7 @@ TEST_CASE_FIXTURE(NormalizeFixture, "trivial_intersection_inhabited") const NormalizedType* n = normalizer.normalize(c); REQUIRE(n); - CHECK(normalizer.isInhabited(n)); + CHECK(normalizer.isInhabited(n) == NormalizationResult::True); } TEST_CASE_FIXTURE(NormalizeFixture, "bare_negated_boolean") diff --git a/tests/RequireByString.test.cpp b/tests/RequireByString.test.cpp index 39ed571ef..99e562a82 100644 --- a/tests/RequireByString.test.cpp +++ b/tests/RequireByString.test.cpp @@ -13,6 +13,46 @@ #include #include +#if __APPLE__ +#include +#if TARGET_OS_IPHONE +#include + +std::optional getResourcePath0() +{ + CFBundleRef mainBundle = CFBundleGetMainBundle(); + if (mainBundle == NULL) + { + return std::nullopt; + } + CFURLRef mainBundleURL = CFBundleCopyBundleURL(mainBundle); + if (mainBundleURL == NULL) + { + CFRelease(mainBundle); + return std::nullopt; + } + + char pathBuffer[PATH_MAX]; + if (!CFURLGetFileSystemRepresentation(mainBundleURL, true, (UInt8*)pathBuffer, PATH_MAX)) + { + CFRelease(mainBundleURL); + CFRelease(mainBundle); + return std::nullopt; + } + + CFRelease(mainBundleURL); + CFRelease(mainBundle); + return std::string(pathBuffer); +} + +std::optional getResourcePath() +{ + static std::optional path0 = getResourcePath0(); + return path0; +} +#endif +#endif + LUAU_FASTFLAG(LuauUpdatedRequireByStringSemantics) class ReplWithPathFixture @@ -49,7 +89,24 @@ class ReplWithPathFixture std::string luauDirRel = "."; std::string luauDirAbs; +#if TARGET_OS_IPHONE + std::optional cwd0 = getCurrentWorkingDirectory(); + std::optional cwd = getResourcePath(); + if (cwd && cwd0) + { + // when running in xcode cwd0 is "/", however that is not always the case + const auto& _res = *cwd; + const auto& _cwd = *cwd0; + if (_res.find(_cwd) == 0) + { + // we need relative path so we subtract cwd0 from cwd + luauDirRel = "./" + _res.substr(_cwd.length()); + } + } +#else std::optional cwd = getCurrentWorkingDirectory(); +#endif + REQUIRE_MESSAGE(cwd, "Error getting Luau path"); std::replace((*cwd).begin(), (*cwd).end(), '\\', '/'); luauDirAbs = *cwd; diff --git a/tests/SharedCodeAllocator.test.cpp b/tests/SharedCodeAllocator.test.cpp index 70cc4d754..983b077c1 100644 --- a/tests/SharedCodeAllocator.test.cpp +++ b/tests/SharedCodeAllocator.test.cpp @@ -1,6 +1,10 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/SharedCodeAllocator.h" +#include "Luau/CodeAllocator.h" + +#include "luacodegen.h" + #include "doctest.h" // We explicitly test correctness of self-assignment for some types @@ -10,15 +14,25 @@ using namespace Luau::CodeGen; + +constexpr size_t kBlockSize = 1024 * 1024; +constexpr size_t kMaxTotalSize = 1024 * 1024; + +static const uint8_t fakeCode[1] = {0x00}; + TEST_SUITE_BEGIN("SharedCodeAllocator"); TEST_CASE("NativeModuleRefRefcounting") { - SharedCodeAllocator allocator{}; + if (!luau_codegen_supported()) + return; + + CodeAllocator codeAllocator{kBlockSize, kMaxTotalSize}; + SharedCodeAllocator allocator{&codeAllocator}; REQUIRE(allocator.tryGetNativeModule(ModuleId{0x0a}).empty()); - NativeModuleRef modRefA = allocator.getOrInsertNativeModule(ModuleId{0x0a}, {}, {}, {}); + NativeModuleRef modRefA = allocator.getOrInsertNativeModule(ModuleId{0x0a}, {}, nullptr, 0, fakeCode, std::size(fakeCode)).first; REQUIRE(!modRefA.empty()); // If we attempt to get the module again, we should get the same module back: @@ -26,14 +40,14 @@ TEST_CASE("NativeModuleRefRefcounting") // If we try to insert another instance of the module, we should get the // existing module back: - REQUIRE(allocator.getOrInsertNativeModule(ModuleId{0x0a}, {}, {}, {}).get() == modRefA.get()); + REQUIRE(allocator.getOrInsertNativeModule(ModuleId{0x0a}, {}, nullptr, 0, fakeCode, std::size(fakeCode)).first.get() == modRefA.get()); // If we try to look up a different module, we should not get the existing // module back: REQUIRE(allocator.tryGetNativeModule(ModuleId{0x0b}).empty()); // (Insert a second module to help with validation below) - NativeModuleRef modRefB = allocator.getOrInsertNativeModule(ModuleId{0x0b}, {}, {}, {}); + NativeModuleRef modRefB = allocator.getOrInsertNativeModule(ModuleId{0x0b}, {}, nullptr, 0, fakeCode, std::size(fakeCode)).first; REQUIRE(!modRefB.empty()); REQUIRE(modRefB.get() != modRefA.get()); @@ -226,41 +240,40 @@ TEST_CASE("NativeModuleRefRefcounting") TEST_CASE("NativeProtoRefcounting") { - SharedCodeAllocator allocator{}; + if (!luau_codegen_supported()) + return; + + CodeAllocator codeAllocator{kBlockSize, kMaxTotalSize}; + SharedCodeAllocator allocator{&codeAllocator}; - std::vector nativeProtos; + std::vector nativeProtos; nativeProtos.reserve(1); - nativeProtos.push_back(NativeProto{0x01, createNativeProtoExecData(0)}); + NativeProtoExecDataPtr nativeProto = createNativeProtoExecData(0); + getNativeProtoExecDataHeader(nativeProto.get()).bytecodeId = 0x01; + nativeProtos.push_back(std::move(nativeProto)); - NativeModuleRef modRefA = allocator.getOrInsertNativeModule(ModuleId{0x0a}, std::move(nativeProtos), {}, {}); + NativeModuleRef modRefA = + allocator.getOrInsertNativeModule(ModuleId{0x0a}, std::move(nativeProtos), nullptr, 0, fakeCode, std::size(fakeCode)).first; REQUIRE(!modRefA.empty()); - REQUIRE(modRefA->getRefcount()); - - const NativeProto* proto1 = modRefA->tryGetNativeProto(0x01); - REQUIRE(proto1 != nullptr); - - // getNonOwningPointerToInstructionOffsets should not acquire ownership: - const uint32_t* unownedInstructionOffsets = proto1->getNonOwningPointerToInstructionOffsets(); - REQUIRE(unownedInstructionOffsets != nullptr); REQUIRE(modRefA->getRefcount() == 1); - // getOwningPointerToInstructionOffsets should acquire ownership: - const uint32_t* ownedInstructionOffsets = proto1->getOwningPointerToInstructionOffsets(); - REQUIRE(ownedInstructionOffsets == unownedInstructionOffsets); + // Verify behavior of addRef: + modRefA->addRef(); REQUIRE(modRefA->getRefcount() == 2); - // We should be able to call it multiple times to get multiple references: - const uint32_t* ownedInstructionOffsets2 = proto1->getOwningPointerToInstructionOffsets(); - REQUIRE(ownedInstructionOffsets2 == unownedInstructionOffsets); + // Verify behavior of addRefs: + modRefA->addRefs(2); + REQUIRE(modRefA->getRefcount() == 4); + + // Undo two of our addRef(s): + modRefA->release(); REQUIRE(modRefA->getRefcount() == 3); - // releaseOwningPointerToInstructionOffsets should be callable to release - // the reference: - NativeProto::releaseOwningPointerToInstructionOffsets(ownedInstructionOffsets2); + modRefA->release(); REQUIRE(modRefA->getRefcount() == 2); // If we release our NativeModuleRef, the module should be kept alive by - // the owning instruction offsets pointer: + // the owning reference we acquired: modRefA.reset(); modRefA = allocator.tryGetNativeModule(ModuleId{0x0a}); @@ -269,62 +282,65 @@ TEST_CASE("NativeProtoRefcounting") // If the last "release" comes via releaseOwningPointerToInstructionOffsets, // the module should be successfully destroyed: + const NativeModule* rawModA = modRefA.get(); + modRefA.reset(); - NativeProto::releaseOwningPointerToInstructionOffsets(ownedInstructionOffsets); + rawModA->release(); REQUIRE(allocator.tryGetNativeModule(ModuleId{0x0a}).empty()); } TEST_CASE("NativeProtoState") { - SharedCodeAllocator allocator{}; + if (!luau_codegen_supported()) + return; + + CodeAllocator codeAllocator{kBlockSize, kMaxTotalSize}; + SharedCodeAllocator allocator{&codeAllocator}; const std::vector data(16); const std::vector code(16); - std::vector nativeProtos; + std::vector nativeProtos; nativeProtos.reserve(2); { - NativeProtoExecDataPtr nativeExecData = createNativeProtoExecData(2); - nativeExecData[0] = 0; - nativeExecData[1] = 4; + NativeProtoExecDataPtr nativeProto = createNativeProtoExecData(2); + getNativeProtoExecDataHeader(nativeProto.get()).bytecodeId = 1; + getNativeProtoExecDataHeader(nativeProto.get()).entryOffsetOrAddress = reinterpret_cast(0x00); + nativeProto[0] = 0; + nativeProto[1] = 4; - NativeProto proto{1, std::move(nativeExecData)}; - proto.setEntryOffset(0x00); - nativeProtos.push_back(std::move(proto)); + nativeProtos.push_back(std::move(nativeProto)); } { - NativeProtoExecDataPtr nativeExecData = createNativeProtoExecData(2); - nativeExecData[0] = 8; - nativeExecData[1] = 12; + NativeProtoExecDataPtr nativeProto = createNativeProtoExecData(2); + getNativeProtoExecDataHeader(nativeProto.get()).bytecodeId = 3; + getNativeProtoExecDataHeader(nativeProto.get()).entryOffsetOrAddress = reinterpret_cast(0x08); + nativeProto[0] = 8; + nativeProto[1] = 12; - NativeProto proto{3, std::move(nativeExecData)}; - proto.setEntryOffset(0x08); - nativeProtos.push_back(std::move(proto)); + nativeProtos.push_back(std::move(nativeProto)); } - NativeModuleRef modRefA = allocator.getOrInsertNativeModule(ModuleId{0x0a}, std::move(nativeProtos), data, code); + NativeModuleRef modRefA = + allocator.getOrInsertNativeModule(ModuleId{0x0a}, std::move(nativeProtos), data.data(), data.size(), code.data(), code.size()).first; REQUIRE(!modRefA.empty()); REQUIRE(modRefA->getModuleBaseAddress() != nullptr); - const NativeProto* proto1 = modRefA->tryGetNativeProto(1); + const uint32_t* proto1 = modRefA->tryGetNativeProto(1); REQUIRE(proto1 != nullptr); - REQUIRE(proto1->getBytecodeId() == 1); - REQUIRE(proto1->getEntryAddress() == modRefA->getModuleBaseAddress() + 0x00); - const uint32_t* proto1Offsets = proto1->getNonOwningPointerToInstructionOffsets(); - REQUIRE(proto1Offsets != nullptr); - REQUIRE(proto1Offsets[0] == 0); - REQUIRE(proto1Offsets[1] == 4); - - const NativeProto* proto3 = modRefA->tryGetNativeProto(3); + REQUIRE(getNativeProtoExecDataHeader(proto1).bytecodeId == 1); + REQUIRE(getNativeProtoExecDataHeader(proto1).entryOffsetOrAddress == modRefA->getModuleBaseAddress() + 0x00); + REQUIRE(proto1[0] == 0); + REQUIRE(proto1[1] == 4); + + const uint32_t* proto3 = modRefA->tryGetNativeProto(3); REQUIRE(proto3 != nullptr); - REQUIRE(proto3->getBytecodeId() == 3); - REQUIRE(proto3->getEntryAddress() == modRefA->getModuleBaseAddress() + 0x08); - const uint32_t* proto3Offsets = proto3->getNonOwningPointerToInstructionOffsets(); - REQUIRE(proto3Offsets != nullptr); - REQUIRE(proto3Offsets[0] == 8); - REQUIRE(proto3Offsets[1] == 12); + REQUIRE(getNativeProtoExecDataHeader(proto3).bytecodeId == 3); + REQUIRE(getNativeProtoExecDataHeader(proto3).entryOffsetOrAddress == modRefA->getModuleBaseAddress() + 0x08); + REQUIRE(proto3[0] == 8); + REQUIRE(proto3[1] == 12); // Ensure that non-existent native protos cannot be found: REQUIRE(modRefA->tryGetNativeProto(0) == nullptr); diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index 52930087e..24ccd296c 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -202,7 +202,7 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type 'bad' could not be converted into 'T'; at [read "v"], string is not exactly number)"; + const std::string expected = R"(Type '{ v: string }' could not be converted into 'T'; at [read "v"], string is not exactly number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}}); CHECK_EQ(expected, toString(result.errors[0])); } @@ -221,7 +221,7 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases") )"); LUAU_REQUIRE_ERROR_COUNT(1, result); - const std::string expected = R"(Type 'bad' could not be converted into 'U'; at [read "t"][read "v"], string is not exactly number)"; + const std::string expected = R"(Type '{ t: { v: string } }' could not be converted into 'U'; at [read "t"][read "v"], string is not exactly number)"; CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}}); CHECK_EQ(expected, toString(result.errors[0])); diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index 000f0eccd..f18c3d7f8 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -463,7 +463,7 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'B'; at [read \"x\"], ChildClass is not exactly BaseClass"); + CHECK(toString(result.errors.at(0)) == "Type 'A' could not be converted into 'B'; at [read \"x\"], ChildClass is not exactly BaseClass"); else { const std::string expected = R"(Type 'A' could not be converted into 'B' @@ -707,4 +707,64 @@ TEST_CASE_FIXTURE(ClassFixture, "cannot_index_a_class_with_no_indexer") CHECK(builtinTypes->errorType == requireType("c")); } + +TEST_CASE_FIXTURE(ClassFixture, "cyclic_tables_are_assumed_to_be_compatible_with_classes") +{ + /* + * This is technically documenting a case where we are intentionally + * unsound. + * + * Our builtins are essentially defined like so: + * + * declare class BaseClass + * BaseField: number + * function BaseMethod(self, number): () + * read Touched: Connection + * end + * + * declare class Connection + * Connect: (Connection, (BaseClass) -> ()) -> () + * end + * + * The type we infer for `onTouch` is + * + * (t1) -> () where t1 = { read BaseField: unknown, read BaseMethod: (t1, number) -> () } + * + * In order to validate that onTouch can be passed to Connect, we must + * verify the following relation: + * + * BaseClass <: t1 where t1 = { read BaseField: unknown, read BaseMethod: (t1, number) -> () } + * + * However, the cycle between the table and the function gums up the works + * here and the worst thing is that it's perfectly reasonable in principle. + * Just from these types, we cannot see that BaseMethod will only be passed + * t1. Without that guarantee, BaseClass cannot be used as a subtype of t1. + * + * I think the theoretically-correct way to untangle this would be to infer + * t1 as a bounded existential type. + * + * For now, we have a subtyping has a rule that provisionally substitutes + * the table for the class type when performing the subtyping test. We + * essentially assume that, for all cyclic functions, that the table and the + * class are mutually subtypes of one another. + * + * For more information, read uses of Subtyping::substitutions. + */ + + CheckResult result = check(R"( + local c = BaseClass.New() + + function requiresNothing() end + + function onTouch(other) + requiresNothing(other:BaseMethod(0)) + print(other.BaseField) + end + + c.Touched:Connect(onTouch) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index 44eff77db..99df053f1 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -2439,9 +2439,41 @@ TEST_CASE_FIXTURE(Fixture, "dont_infer_overloaded_functions") LUAU_REQUIRE_NO_ERRORS(result); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK("(t1) -> () where t1 = { read FindFirstChild: (t1, string) -> (a...) }" == toString(requireType("getR6Attachments"))); + CHECK("(t1) -> () where t1 = { read FindFirstChild: (t1, string) -> (...unknown) }" == toString(requireType("getR6Attachments"))); else CHECK("(t1) -> () where t1 = {+ FindFirstChild: (t1, string) -> (a...) +}" == toString(requireType("getR6Attachments"))); } +TEST_CASE_FIXTURE(Fixture, "param_y_is_bounded_by_x_of_type_string") +{ + CheckResult result = check(R"( + local function f(x: string, y) + x = y + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK("(string, string) -> ()" == toString(requireType("f"))); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "function_that_could_return_anything_is_compatible_with_function_that_is_expected_to_return_nothing") +{ + CheckResult result = check(R"( + -- We infer foo : (g: (number) -> (...unknown)) -> () + function foo(g) + g(0) + end + + -- a requires a function that returns no values + function a(f: ((number) -> ()) -> ()) + end + + -- "Returns an unknown number of values" is close enough to "returns no values." + a(foo) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index be6e15dca..93fbc945b 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -828,7 +828,7 @@ y.a.c = y if (FFlag::DebugLuauDeferredConstraintResolution) CHECK( toString(result.errors.at(0)) == - R"(Type 'x' could not be converted into 'T'; type x[read "a"][read "c"] (nil) is not exactly T[read "a"][read "c"][0] (T))"); + R"(Type '{ a: { c: nil, d: number }, b: number }' could not be converted into 'T'; type { a: { c: nil, d: number }, b: number }[read "a"][read "c"] (nil) is not exactly T[read "a"][read "c"][0] (T))"); else { const std::string expected = R"(Type 'y' could not be converted into 'T' diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index 97efef16f..dd8093745 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -414,7 +414,7 @@ local b: B.T = a LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [read \"x\"], number is not exactly string"); + CHECK(toString(result.errors.at(0)) == "Type 'T' could not be converted into 'T'; at [read \"x\"], number is not exactly string"); else { const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B' @@ -455,7 +455,7 @@ local b: B.T = a LUAU_REQUIRE_ERROR_COUNT(1, result); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK(toString(result.errors.at(0)) == "Type 'a' could not be converted into 'T'; at [read \"x\"], number is not exactly string"); + CHECK(toString(result.errors.at(0)) == "Type 'T' could not be converted into 'T'; at [read \"x\"], number is not exactly string"); else { const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C' diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index da23e3f2a..64c263c3a 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -1105,6 +1105,45 @@ foo(1 :: any) LUAU_REQUIRE_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "luau_roact_useState_nilable_state_1") +{ + ScopedFastFlag sff{FFlag::DebugLuauDeferredConstraintResolution, true}; + + CheckResult result = check(R"( + type Dispatch = (A) -> () + type BasicStateAction = ((S) -> S) | S + + type ScriptConnection = { Disconnect: (ScriptConnection) -> () } + + local blah = nil :: any + + local function useState( + initialState: (() -> S) | S, + ... + ): (S, Dispatch>) + return blah, blah + end + + local a, b = useState(nil :: ScriptConnection?) + + if a then + a:Disconnect() + b(nil :: ScriptConnection?) + end + )"); + + if (FFlag::DebugLuauDeferredConstraintResolution) + LUAU_REQUIRE_NO_ERRORS(result); + else + { + // This is a known bug in the old solver. + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK(Location{{19, 14}, {19, 41}} == result.errors[0].location); + } +} + TEST_CASE_FIXTURE(BuiltinsFixture, "luau_roact_useState_minimization") { // We don't expect this test to work on the old solver, but it also does not yet work on the new solver. diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index 81cfabd9c..49bca197b 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -228,7 +228,7 @@ TEST_CASE_FIXTURE(Fixture, "tagged_unions_immutable_tag") type Dog = { tag: "Dog", howls: boolean } type Cat = { tag: "Cat", meows: boolean } type Animal = Dog | Cat - local a : Animal = { tag = "Cat", meows = true } + local a: Animal = { tag = "Cat", meows = true } a.tag = "Dog" )"); @@ -365,8 +365,10 @@ TEST_CASE_FIXTURE(Fixture, "parametric_tagged_union_alias") LUAU_REQUIRE_ERROR_COUNT(1, result); - // FIXME: This could be improved by expanding the contents of `a` - const std::string expectedError = "Type 'a' could not be converted into 'Err | Ok'"; + const std::string expectedError = R"(Type + '{ result: string, success: boolean }' +could not be converted into + 'Err | Ok')"; CHECK(toString(result.errors[0]) == expectedError); } @@ -539,4 +541,26 @@ TEST_CASE_FIXTURE(Fixture, "no_widening_from_callsites") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "singletons_stick_around_under_assignment") +{ + CheckResult result = check(R"( + type Foo = { + kind: "Foo", + } + + local foo = (nil :: any) :: Foo + + print(foo.kind == "Bar") -- TypeError: Type "Foo" cannot be compared with "Bar" + local kind = foo.kind + print(kind == "Bar") -- SHOULD BE: TypeError: Type "Foo" cannot be compared with "Bar" + )"); + + // FIXME: Under the new solver, we get both the errors we expect, but they're + // duplicated because of how we are currently running type family reduction. + if (FFlag::DebugLuauDeferredConstraintResolution) + LUAU_REQUIRE_ERROR_COUNT(4, result); + else + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 8bc2a5414..5d5d6042c 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -2174,7 +2174,7 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at [read "y"], number is not exactly string)"); + CHECK(toString(result.errors.at(0)) == R"(Type 'A' could not be converted into 'B'; at [read "y"], number is not exactly string)"); else { const std::string expected = R"(Type 'A' could not be converted into 'B' @@ -2201,7 +2201,7 @@ local b: B = a LUAU_REQUIRE_ERRORS(result); if (FFlag::DebugLuauDeferredConstraintResolution) - CHECK(toString(result.errors.at(0)) == R"(Type 'a' could not be converted into 'B'; at [read "b"][read "y"], number is not exactly string)"); + CHECK(toString(result.errors.at(0)) == R"(Type 'A' could not be converted into 'B'; at [read "b"][read "y"], number is not exactly string)"); else { const std::string expected = R"(Type 'A' could not be converted into 'B' @@ -3994,7 +3994,7 @@ TEST_CASE_FIXTURE(Fixture, "identify_all_problematic_table_fields") LUAU_REQUIRE_ERROR_COUNT(1, result); - std::string expected = "Type 'a' could not be converted into 'T'; at [read \"a\"], string is not exactly number" + std::string expected = "Type '{ a: string, b: boolean, c: number }' could not be converted into 'T'; at [read \"a\"], string is not exactly number" "\n\tat [read \"b\"], boolean is not exactly string" "\n\tat [read \"c\"], number is not exactly boolean"; CHECK(toString(result.errors[0]) == expected); diff --git a/tests/TypeInfer.typestates.test.cpp b/tests/TypeInfer.typestates.test.cpp index bee1b6876..0f6ddfa07 100644 --- a/tests/TypeInfer.typestates.test.cpp +++ b/tests/TypeInfer.typestates.test.cpp @@ -25,7 +25,7 @@ TEST_CASE_FIXTURE(TypeStateFixture, "initialize_x_of_type_string_or_nil_with_nil )"); LUAU_REQUIRE_NO_ERRORS(result); - CHECK("nil" == toString(requireType("a"))); + CHECK("string?" == toString(requireType("a"))); } TEST_CASE_FIXTURE(TypeStateFixture, "extraneous_lvalues_are_populated_with_nil") @@ -55,7 +55,7 @@ TEST_CASE_FIXTURE(TypeStateFixture, "assign_different_values_to_x") )"); LUAU_REQUIRE_NO_ERRORS(result); - CHECK("nil" == toString(requireType("a"))); + CHECK("string?" == toString(requireType("a"))); CHECK("string" == toString(requireType("b"))); } @@ -73,8 +73,28 @@ TEST_CASE_FIXTURE(TypeStateFixture, "parameter_x_was_constrained_by_two_types") end )"); - LUAU_REQUIRE_NO_ERRORS(result); - CHECK("(string) -> string?" == toString(requireType("f"))); + if (FFlag::DebugLuauDeferredConstraintResolution) + { + // `y` is annotated `string | number` which is explicitly not compatible with `string?` + // as such, we produce an error here for that mismatch. + // + // this is not necessarily the best inference here, since we can indeed produce `string` + // as a type for `x`, but it's a limitation we can accept for now. + LUAU_REQUIRE_ERRORS(result); + + TypePackMismatch* tpm = get(result.errors[0]); + REQUIRE(tpm); + CHECK("string?" == toString(tpm->wantedTp)); + CHECK("number | string" == toString(tpm->givenTp)); + + CHECK("(number | string) -> string?" == toString(requireType("f"))); + } + else + { + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK("(string) -> string?" == toString(requireType("f"))); + } } #if 0 @@ -451,4 +471,24 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "typestates_preserve_error_suppression_proper CHECK("*error-type* | string" == toString(requireTypeAtPosition({3, 16}), {true})); } +TEST_CASE_FIXTURE(BuiltinsFixture, "typestates_do_not_apply_to_the_initial_local_definition") +{ + // early return if the flag isn't set since this is blocking gated commits + if (!FFlag::DebugLuauDeferredConstraintResolution) + return; + + CheckResult result = check(R"( + type MyType = number | string + local foo: MyType = 5 + print(foo) + foo = 7 + print(foo) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + CHECK("number | string" == toString(requireTypeAtPosition({3, 14}), {true})); + CHECK("number" == toString(requireTypeAtPosition({5, 14}), {true})); +} + + TEST_SUITE_END(); diff --git a/tools/faillist.txt b/tools/faillist.txt index 89e27f1fd..ce91bcc45 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -1,7 +1,6 @@ AstQuery.last_argument_function_call_type AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg -AutocompleteTest.autocomplete_string_singleton_equality AutocompleteTest.autocomplete_string_singletons AutocompleteTest.do_wrong_compatible_nonself_calls AutocompleteTest.string_singleton_as_table_key @@ -34,11 +33,7 @@ BuiltinTests.string_format_use_correct_argument2 BuiltinTests.table_freeze_is_generic BuiltinTests.tonumber_returns_optional_number_type ControlFlowAnalysis.tagged_unions -DefinitionTests.class_definition_indexer DefinitionTests.class_definition_overload_metamethods -DefinitionTests.class_definition_string_props -DefinitionTests.declaring_generic_functions -DefinitionTests.definition_file_classes Differ.metatable_metamissing_left Differ.metatable_metamissing_right Differ.metatable_metanormal @@ -81,7 +76,6 @@ GenericsTests.properties_can_be_instantiated_polytypes GenericsTests.quantify_functions_even_if_they_have_an_explicit_generic GenericsTests.self_recursive_instantiated_param GenericsTests.type_parameters_can_be_polytypes -GenericsTests.typefuns_sharing_types IntersectionTypes.CLI-44817 IntersectionTypes.error_detailed_intersection_all IntersectionTypes.error_detailed_intersection_part @@ -108,10 +102,8 @@ IntersectionTypes.overloadeded_functions_with_weird_typepacks_3 IntersectionTypes.overloadeded_functions_with_weird_typepacks_4 IntersectionTypes.table_write_sealed_indirect IntersectionTypes.union_saturate_overloaded_functions -Linter.FormatStringTyped Linter.TableOperationsIndexer ModuleTests.clone_self_property -Negations.negated_string_is_a_subtype_of_string NonstrictModeTests.inconsistent_module_return_types_are_ok NonstrictModeTests.infer_nullary_function NonstrictModeTests.infer_the_maximum_number_of_values_the_function_could_return @@ -143,7 +135,6 @@ ProvisionalTests.table_insert_with_a_singleton_argument ProvisionalTests.table_unification_infinite_recursion ProvisionalTests.typeguard_inference_incomplete ProvisionalTests.while_body_are_also_refined -RefinementTest.assert_a_to_be_truthy_then_assert_a_to_be_number RefinementTest.call_an_incompatible_function_after_using_typeguard RefinementTest.dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never RefinementTest.discriminate_from_isa_of_x @@ -159,8 +150,6 @@ RefinementTest.refine_a_param_that_got_resolved_during_constraint_solving_stage RefinementTest.refine_a_property_of_some_global RefinementTest.refine_param_of_type_folder_or_part_without_using_typeof RefinementTest.refine_unknown_to_table_then_clone_it -RefinementTest.refinements_should_preserve_error_suppression -RefinementTest.string_not_equal_to_string_or_nil RefinementTest.truthy_constraint_on_properties RefinementTest.type_annotations_arent_relevant_when_doing_dataflow_analysis RefinementTest.type_guard_narrowed_into_nothingness @@ -196,17 +185,13 @@ TableTests.explicitly_typed_table_error TableTests.explicitly_typed_table_with_indexer TableTests.generalize_table_argument TableTests.generic_table_instantiation_potential_regression -TableTests.indexer_mismatch TableTests.indexer_on_sealed_table_must_unify_with_free_table TableTests.indexers_get_quantified_too TableTests.infer_indexer_from_array_like_table TableTests.infer_indexer_from_its_variable_type_and_unifiable TableTests.inferred_return_type_of_free_table -TableTests.instantiate_table_cloning_3 TableTests.invariant_table_properties_means_instantiating_tables_in_assignment_is_unsound TableTests.invariant_table_properties_means_instantiating_tables_in_call_is_unsound -TableTests.length_operator_intersection -TableTests.length_operator_non_table_union TableTests.length_operator_union TableTests.less_exponential_blowup_please TableTests.meta_add @@ -226,13 +211,10 @@ TableTests.parameter_was_set_an_indexer_and_bounded_by_string TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2 TableTests.persistent_sealed_table_is_immutable -TableTests.prop_access_on_key_whose_types_mismatches -TableTests.prop_access_on_unions_of_indexers_where_key_whose_types_mismatches TableTests.quantify_even_that_table_was_never_exported_at_all TableTests.quantify_metatables_of_metatables_of_table TableTests.reasonable_error_when_adding_a_nonexistent_property_to_an_array_like_table TableTests.recursive_metatable_type_call -TableTests.result_is_always_any_if_lhs_is_any TableTests.right_table_missing_key2 TableTests.scalar_is_a_subtype_of_a_compatible_polymorphic_shape_type TableTests.scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type @@ -251,7 +233,6 @@ TableTests.table_subtyping_with_missing_props_dont_report_multiple_errors2 TableTests.table_unification_4 TableTests.table_unifies_into_map TableTests.type_mismatch_on_massive_table_is_cut_short -TableTests.unification_of_unions_in_a_self_referential_type TableTests.used_colon_instead_of_dot TableTests.used_dot_instead_of_colon TableTests.when_augmenting_an_unsealed_table_with_an_indexer_apply_the_correct_scope_to_the_indexer_type @@ -262,7 +243,6 @@ ToString.named_metatable_toStringNamedFunction ToString.no_parentheses_around_cyclic_function_type_in_intersection ToString.pick_distinct_names_for_mixed_explicit_and_implicit_generics ToString.primitive -ToString.tostring_unsee_ttv_if_array ToString.toStringDetailed2 ToString.toStringErrorPack TryUnifyTests.members_of_failed_typepack_unification_are_unified_with_errorType @@ -278,19 +258,13 @@ TypeAliases.mutually_recursive_types_restriction_not_ok_2 TypeAliases.mutually_recursive_types_swapsies_not_ok TypeAliases.recursive_types_restriction_not_ok TypeAliases.report_shadowed_aliases -TypeAliases.saturate_to_first_type_pack TypeAliases.type_alias_local_mutation TypeAliases.type_alias_local_rename TypeAliases.type_alias_locations TypeAliases.type_alias_of_an_imported_recursive_generic_type -TypeAliases.use_table_name_and_generic_params_in_errors TypeFamilyTests.add_family_at_work TypeFamilyTests.family_as_fn_arg -TypeFamilyTests.family_as_fn_ret -TypeFamilyTests.function_internal_families TypeFamilyTests.internal_families_raise_errors -TypeFamilyTests.table_internal_families -TypeFamilyTests.type_families_inhabited_with_normalization TypeFamilyTests.unsolvable_family TypeInfer.be_sure_to_use_active_txnlog_when_evaluating_a_variadic_overload TypeInfer.check_type_infer_recursion_count @@ -305,34 +279,24 @@ TypeInfer.globals2 TypeInfer.globals_are_banned_in_strict_mode TypeInfer.infer_through_group_expr TypeInfer.no_stack_overflow_from_isoptional -TypeInfer.promote_tail_type_packs TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2 -TypeInfer.stringify_nested_unions_with_optionals TypeInfer.tc_after_error_recovery_no_replacement_name_in_error TypeInfer.type_infer_recursion_limit_no_ice TypeInfer.type_infer_recursion_limit_normalizer TypeInfer.unify_nearly_identical_recursive_types -TypeInferAnyError.any_type_propagates -TypeInferAnyError.assign_prop_to_table_by_calling_any_yields_any -TypeInferAnyError.call_to_any_yields_any TypeInferAnyError.can_subscript_any TypeInferAnyError.for_in_loop_iterator_is_error TypeInferAnyError.for_in_loop_iterator_is_error2 TypeInferAnyError.metatable_of_any_can_be_a_table -TypeInferAnyError.quantify_any_does_not_bind_to_itself TypeInferAnyError.replace_every_free_type_when_unifying_a_complex_function_with_any TypeInferClasses.callable_classes TypeInferClasses.cannot_unify_class_instance_with_primitive TypeInferClasses.class_type_mismatch_with_name_conflict -TypeInferClasses.class_unification_type_mismatch_is_correct_order TypeInferClasses.detailed_class_unification_error TypeInferClasses.indexable_classes -TypeInferClasses.intersections_of_unions_of_classes -TypeInferClasses.optional_class_field_access_error TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties TypeInferClasses.table_indexers_are_invariant -TypeInferClasses.unions_of_intersections_of_classes TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class TypeInferFunctions.another_other_higher_order_function TypeInferFunctions.bidirectional_checking_of_callback_property @@ -364,11 +328,9 @@ TypeInferFunctions.infer_anonymous_function_arguments TypeInferFunctions.infer_anonymous_function_arguments_outside_call TypeInferFunctions.infer_generic_function_function_argument TypeInferFunctions.infer_generic_function_function_argument_overloaded -TypeInferFunctions.infer_return_type_from_selected_overload TypeInferFunctions.infer_return_value_type TypeInferFunctions.inferred_higher_order_functions_are_quantified_at_the_right_time3 TypeInferFunctions.instantiated_type_packs_must_have_a_non_null_scope -TypeInferFunctions.list_all_overloads_if_no_overload_takes_given_argument_count TypeInferFunctions.list_only_alternative_overloads_that_match_argument_count TypeInferFunctions.luau_subtyping_is_np_hard TypeInferFunctions.no_lossy_function_type @@ -376,7 +338,6 @@ TypeInferFunctions.occurs_check_failure_in_function_return_type TypeInferFunctions.other_things_are_not_related_to_function TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2 -TypeInferFunctions.record_matching_overload TypeInferFunctions.report_exiting_without_return_nonstrict TypeInferFunctions.return_type_by_overload TypeInferFunctions.too_few_arguments_variadic @@ -396,13 +357,10 @@ TypeInferLoops.for_in_loop_error_on_iterator_requiring_args_but_none_given TypeInferLoops.for_in_loop_on_error TypeInferLoops.for_in_loop_on_non_function TypeInferLoops.for_in_loop_with_next -TypeInferLoops.for_in_with_an_iterator_of_type_any TypeInferLoops.for_loop TypeInferLoops.ipairs_produces_integral_indices TypeInferLoops.iterate_over_free_table TypeInferLoops.iterate_over_properties -TypeInferLoops.iteration_no_table_passed -TypeInferLoops.iteration_regression_issue_69967 TypeInferLoops.iteration_regression_issue_69967_alt TypeInferLoops.loop_iter_metamethod_nil TypeInferLoops.loop_iter_metamethod_ok @@ -415,11 +373,8 @@ TypeInferLoops.repeat_loop TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free TypeInferLoops.while_loop TypeInferModules.custom_require_global -TypeInferModules.do_not_modify_imported_types TypeInferModules.do_not_modify_imported_types_5 TypeInferModules.require -TypeInferOOP.CheckMethodsOfSealed -TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_another_overload_works TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2 TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon TypeInferOOP.inferring_hundreds_of_self_calls_should_not_suffocate_memory @@ -432,12 +387,9 @@ TypeInferOperators.compound_assign_result_must_be_compatible_with_var TypeInferOperators.concat_op_on_free_lhs_and_string_rhs TypeInferOperators.concat_op_on_string_lhs_and_free_rhs TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops -TypeInferOperators.equality_operations_succeed_if_any_union_branch_succeeds -TypeInferOperators.error_on_invalid_operand_types_to_relational_operators2 TypeInferOperators.luau_polyfill_is_array TypeInferOperators.mm_comparisons_must_return_a_boolean TypeInferOperators.reworked_and -TypeInferOperators.reworked_or TypeInferOperators.strict_binary_op_where_lhs_unknown TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs @@ -445,7 +397,6 @@ TypeInferOperators.typecheck_unary_len_error TypeInferOperators.typecheck_unary_minus_error TypeInferOperators.UnknownGlobalCompoundAssign TypeInferPrimitives.CheckMethodsOfNumber -TypeInferPrimitives.string_function_indirect TypeInferPrimitives.string_index TypeInferUnknownNever.assign_to_local_which_is_never TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_never @@ -457,7 +408,6 @@ TypePackTests.fuzz_typepack_iter_follow_2 TypePackTests.pack_tail_unification_check TypePackTests.type_alias_backwards_compatible TypePackTests.type_alias_default_type_errors -TypePackTests.type_alias_type_packs_import TypePackTests.unify_variadic_tails_in_arguments TypeSingletons.enums_using_singletons_mismatch TypeSingletons.error_detailed_tagged_union_mismatch_bool @@ -465,6 +415,7 @@ TypeSingletons.error_detailed_tagged_union_mismatch_string TypeSingletons.overloaded_function_call_with_singletons_mismatch TypeSingletons.return_type_of_f_is_not_widened TypeSingletons.table_properties_type_error_escapes +TypeSingletons.tagged_unions_immutable_tag TypeSingletons.widen_the_supertype_if_it_is_free_and_subtype_has_singleton TypeStatesTest.prototyped_recursive_functions_but_has_future_assignments TypeStatesTest.typestates_preserve_error_suppression_properties