diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index 5f9c45726..def7ab27d 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -157,6 +157,15 @@ struct HasPropConstraint std::string prop; ValueContext context; + // We want to track if this `HasPropConstraint` comes from a conditional. + // If it does, we're going to change the behavior of property look-up a bit. + // In particular, we're going to return `unknownType` for property lookups + // on `table` or inexact table types where the property is not present. + // + // This allows us to refine table types to have additional properties + // without reporting errors in typechecking on the property tests. + bool inConditional = false; + // HACK: We presently need types like true|false or string|"hello" when // deciding whether a particular literal expression should have a singleton // type. This boolean is set to true when extracting the property type of a @@ -193,6 +202,19 @@ struct SetPropConstraint TypeId propType; }; +// resultType ~ hasIndexer subjectType indexType +// +// If the subject type is a table or table-like thing that supports indexing, +// populate the type result with the result type of such an index operation. +// +// If the subject is not indexable, resultType is bound to errorType. +struct HasIndexerConstraint +{ + TypeId resultType; + TypeId subjectType; + TypeId indexType; +}; + // result ~ setIndexer subjectType indexType propType // // If the subject is a table or table-like thing that already has an indexer, @@ -267,7 +289,7 @@ struct ReducePackConstraint using ConstraintV = Variant; struct Constraint diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index 3d3dcb4d6..dec8f1132 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -71,6 +71,8 @@ struct ConstraintGenerator // This is null when the CG is initially constructed. Scope* rootScope; + TypeContext typeContext = TypeContext::Default; + struct InferredBinding { Scope* scope; diff --git a/Analysis/include/Luau/ConstraintSolver.h b/Analysis/include/Luau/ConstraintSolver.h index 54d229c3f..7d762fc06 100644 --- a/Analysis/include/Luau/ConstraintSolver.h +++ b/Analysis/include/Luau/ConstraintSolver.h @@ -131,6 +131,10 @@ struct ConstraintSolver bool tryDispatch(const PrimitiveTypeConstraint& c, NotNull constraint); bool tryDispatch(const HasPropConstraint& c, NotNull constraint); bool tryDispatch(const SetPropConstraint& c, NotNull constraint, bool force); + + bool tryDispatchHasIndexer(int& recursionDepth, NotNull constraint, TypeId subjectType, TypeId indexType, TypeId resultType); + bool tryDispatch(const HasIndexerConstraint& c, NotNull constraint); + bool tryDispatch(const SetIndexerConstraint& c, NotNull constraint, bool force); bool tryDispatch(const SingletonOrTopTypeConstraint& c, NotNull constraint); bool tryDispatch(const UnpackConstraint& c, NotNull constraint); @@ -148,9 +152,9 @@ struct ConstraintSolver TypeId nextTy, TypeId tableTy, TypeId firstIndexTy, const IterableConstraint& c, NotNull constraint, bool force); std::pair, std::optional> lookupTableProp( - TypeId subjectType, const std::string& propName, ValueContext context, bool suppressSimplification = false); + TypeId subjectType, const std::string& propName, ValueContext context, bool inConditional = false, bool suppressSimplification = false); std::pair, std::optional> lookupTableProp( - TypeId subjectType, const std::string& propName, ValueContext context, bool suppressSimplification, DenseHashSet& seen); + TypeId subjectType, const std::string& propName, ValueContext context, bool inConditional, bool suppressSimplification, DenseHashSet& seen); void block(NotNull target, NotNull constraint); /** diff --git a/Analysis/include/Luau/Set.h b/Analysis/include/Luau/Set.h index aeb15aaa2..033bf840a 100644 --- a/Analysis/include/Luau/Set.h +++ b/Analysis/include/Luau/Set.h @@ -1,8 +1,12 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #pragma once +#include "Luau/Common.h" #include "Luau/DenseHash.h" +LUAU_FASTFLAG(LuauFixSetIter) +LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) + namespace Luau { @@ -124,10 +128,15 @@ class Set using difference_type = ptrdiff_t; using iterator_category = std::forward_iterator_tag; - const_iterator(typename Impl::const_iterator impl, typename Impl::const_iterator end) - : impl(impl) - , end(end) + const_iterator(typename Impl::const_iterator impl_, typename Impl::const_iterator end_) + : impl(impl_) + , end(end_) { + if (FFlag::LuauFixSetIter || FFlag::DebugLuauDeferredConstraintResolution) + { + while (impl != end && impl->second == false) + ++impl; + } } const T& operator*() const @@ -140,7 +149,6 @@ class Set return &impl->first; } - bool operator==(const const_iterator& other) const { return impl == other.impl; diff --git a/Analysis/include/Luau/Type.h b/Analysis/include/Luau/Type.h index a0c6b9ccb..25b71e032 100644 --- a/Analysis/include/Luau/Type.h +++ b/Analysis/include/Luau/Type.h @@ -33,6 +33,7 @@ struct Scope; using ScopePtr = std::shared_ptr; struct TypeFamily; +struct Constraint; /** * There are three kinds of type variables: @@ -144,6 +145,10 @@ struct BlockedType { BlockedType(); int index; + + // The constraint that is intended to unblock this type. Other constraints + // should block on this constraint if present. + Constraint* owner = nullptr; }; struct PrimitiveType diff --git a/Analysis/include/Luau/TypeUtils.h b/Analysis/include/Luau/TypeUtils.h index 2fcc8fef5..b4726b5c7 100644 --- a/Analysis/include/Luau/TypeUtils.h +++ b/Analysis/include/Luau/TypeUtils.h @@ -22,6 +22,37 @@ enum class ValueContext RValue }; +/// the current context of the type checker +enum class TypeContext +{ + /// the default context + Default, + /// inside of a condition + Condition, +}; + +bool inConditional(const TypeContext& context); + +// sets the given type context to `Condition` and restores it to its original +// value when the struct drops out of scope +struct InConditionalContext +{ + TypeContext* typeContext; + TypeContext oldValue; + + InConditionalContext(TypeContext* c) + : typeContext(c) + , oldValue(*c) + { + *typeContext = TypeContext::Condition; + } + + ~InConditionalContext() + { + *typeContext = oldValue; + } +}; + using ScopePtr = std::shared_ptr; std::optional findMetatableEntry( diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index d5c947da0..d18ea70d8 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -15,7 +15,6 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); LUAU_FASTFLAGVARIABLE(LuauAutocompleteStringLiteralBounds, false); -LUAU_FASTFLAGVARIABLE(LuauAutocompleteTableKeysNoInitialCharacter, false); static const std::unordered_set kStatementStartingKeywords = { "while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; @@ -1741,7 +1740,7 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M } } } - else if (AstExprTable* exprTable = node->as(); exprTable && FFlag::LuauAutocompleteTableKeysNoInitialCharacter) + else if (AstExprTable* exprTable = node->as()) { AutocompleteEntryMap result; diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index 7208f2ccd..f894d88db 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -1175,7 +1175,10 @@ ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatCompoundAss ControlFlow ConstraintGenerator::visit(const ScopePtr& scope, AstStatIf* ifStatement) { - RefinementId refinement = check(scope, ifStatement->condition, std::nullopt).refinement; + RefinementId refinement = [&](){ + InConditionalContext flipper{&typeContext}; + return check(scope, ifStatement->condition, std::nullopt).refinement; + }(); ScopePtr thenScope = childScope(ifStatement->thenbody, scope); applyRefinements(thenScope, ifStatement->condition->location, refinement); @@ -1910,7 +1913,7 @@ Inference ConstraintGenerator::checkIndexName(const ScopePtr& scope, const Refin scope->rvalueRefinements[key->def] = result; } - addConstraint(scope, indexee->location, HasPropConstraint{result, obj, std::move(index), ValueContext::RValue}); + addConstraint(scope, indexee->location, HasPropConstraint{result, obj, std::move(index), ValueContext::RValue, inConditional(typeContext)}); if (key) return Inference{result, refinementArena.proposition(key, builtinTypes->truthyType)}; @@ -1935,7 +1938,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexExpr* in TypeId obj = check(scope, indexExpr->expr).ty; TypeId indexType = check(scope, indexExpr->index).ty; - TypeId result = freshType(scope); + TypeId result = arena->addType(BlockedType{}); const RefinementKey* key = dfg->getRefinementKey(indexExpr); if (key) @@ -1946,10 +1949,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIndexExpr* in scope->rvalueRefinements[key->def] = result; } - TableIndexer indexer{indexType, result}; - TypeId tableType = arena->addType(TableType{TableType::Props{}, TableIndexer{indexType, result}, TypeLevel{}, scope.get(), TableState::Free}); - - addConstraint(scope, indexExpr->expr->location, SubtypeConstraint{obj, tableType}); + addConstraint(scope, indexExpr->expr->location, HasIndexerConstraint{result, obj, indexType}); if (key) return Inference{result, refinementArena.proposition(key, builtinTypes->truthyType)}; @@ -2200,8 +2200,11 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprBinary* binar Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprIfElse* ifElse, std::optional expectedType) { - ScopePtr condScope = childScope(ifElse->condition, scope); - RefinementId refinement = check(condScope, ifElse->condition).refinement; + RefinementId refinement = [&](){ + InConditionalContext flipper{&typeContext}; + ScopePtr condScope = childScope(ifElse->condition, scope); + return check(condScope, ifElse->condition).refinement; + }(); ScopePtr thenScope = childScope(ifElse->trueExpr, scope); applyRefinements(thenScope, ifElse->trueExpr->location, refinement); @@ -2406,10 +2409,17 @@ std::optional ConstraintGenerator::checkLValue(const ScopePtr& scope, As if (transform) { - addConstraint(scope, local->location, + Constraint* owner = nullptr; + if (auto blocked = get(*ty)) + owner = blocked->owner; + + auto unpackC = addConstraint(scope, local->location, UnpackConstraint{arena->addTypePack({*ty}), arena->addTypePack({assignedTy}), /*resultIsLValue*/ true}); + if (owner) + unpackC->dependencies.push_back(NotNull{owner}); + recordInferredBinding(local->local, *ty); } @@ -2538,6 +2548,7 @@ TypeId ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId updatedType = arena->addType(BlockedType{}); auto setC = addConstraint(scope, expr->location, SetPropConstraint{updatedType, subjectType, std::move(segmentStrings), assignedTy}); + getMutable(updatedType)->owner = setC.get(); TypeId prevSegmentTy = updatedType; for (size_t i = 0; i < segments.size(); ++i) @@ -2545,7 +2556,7 @@ TypeId ConstraintGenerator::updateProperty(const ScopePtr& scope, AstExpr* expr, TypeId segmentTy = arena->addType(BlockedType{}); module->astTypes[exprs[i]] = segmentTy; ValueContext ctx = i == segments.size() - 1 ? ValueContext::LValue : ValueContext::RValue; - auto hasC = addConstraint(scope, expr->location, HasPropConstraint{segmentTy, prevSegmentTy, segments[i], ctx}); + auto hasC = addConstraint(scope, expr->location, HasPropConstraint{segmentTy, prevSegmentTy, segments[i], ctx, inConditional(typeContext)}); setC->dependencies.push_back(hasC); prevSegmentTy = segmentTy; } @@ -2582,16 +2593,12 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, interiorTypes.back().push_back(ty); - auto createIndexer = [this, scope, ttv](const Location& location, TypeId currentIndexType, TypeId currentResultType) { - if (!ttv->indexer) - { - TypeId indexType = this->freshType(scope); - TypeId resultType = this->freshType(scope); - ttv->indexer = TableIndexer{indexType, resultType}; - } + TypeIds indexKeyLowerBound; + TypeIds indexValueLowerBound; - addConstraint(scope, location, SubtypeConstraint{ttv->indexer->indexType, currentIndexType}); - addConstraint(scope, location, SubtypeConstraint{ttv->indexer->indexResultType, currentResultType}); + auto createIndexer = [&indexKeyLowerBound, &indexValueLowerBound](const Location& location, TypeId currentIndexType, TypeId currentResultType) { + indexKeyLowerBound.insert(follow(currentIndexType)); + indexValueLowerBound.insert(follow(currentResultType)); }; std::optional annotatedKeyType; @@ -2633,7 +2640,7 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, expectedValueType = arena->addType(BlockedType{}); addConstraint(scope, item.value->location, HasPropConstraint{ - *expectedValueType, *expectedType, stringKey->value.data, ValueContext::RValue, /*suppressSimplification*/ true}); + *expectedValueType, *expectedType, stringKey->value.data, ValueContext::RValue, /*inConditional*/ inConditional(typeContext), /*suppressSimplification*/ true}); } } } @@ -2705,6 +2712,23 @@ Inference ConstraintGenerator::check(const ScopePtr& scope, AstExprTable* expr, } } + if (!indexKeyLowerBound.empty()) + { + LUAU_ASSERT(!indexValueLowerBound.empty()); + + TypeId indexKey = indexKeyLowerBound.size() == 1 + ? *indexKeyLowerBound.begin() + : arena->addType(UnionType{std::vector(indexKeyLowerBound.begin(), indexKeyLowerBound.end())}) + ; + + TypeId indexValue = indexValueLowerBound.size() == 1 + ? *indexValueLowerBound.begin() + : arena->addType(UnionType{std::vector(indexValueLowerBound.begin(), indexValueLowerBound.end())}) + ; + + ttv->indexer = TableIndexer{indexKey, indexValue}; + } + return Inference{ty}; } diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 00829b856..db7e84c56 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -11,11 +11,13 @@ #include "Luau/ModuleResolver.h" #include "Luau/OverloadResolution.h" #include "Luau/Quantify.h" +#include "Luau/RecursionCounter.h" #include "Luau/Simplify.h" #include "Luau/TimeTrace.h" #include "Luau/ToString.h" #include "Luau/Type.h" #include "Luau/TypeFamily.h" +#include "Luau/TypeFwd.h" #include "Luau/TypeUtils.h" #include "Luau/Unifier2.h" #include "Luau/VecDeque.h" @@ -25,6 +27,8 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false); +LUAU_FASTINTVARIABLE(LuauSolverRecursionLimit, 500); + namespace Luau { @@ -512,6 +516,8 @@ bool ConstraintSolver::tryDispatch(NotNull constraint, bool fo success = tryDispatch(*hpc, constraint); else if (auto spc = get(*constraint)) success = tryDispatch(*spc, constraint, force); + else if (auto spc = get(*constraint)) + success = tryDispatch(*spc, constraint); else if (auto spc = get(*constraint)) success = tryDispatch(*spc, constraint, force); else if (auto sottc = get(*constraint)) @@ -979,6 +985,13 @@ bool ConstraintSolver::tryDispatch(const FunctionCallConstraint& c, NotNull(fn)) + { + asMutable(c.result)->ty.emplace(builtinTypes->neverTypePack); + unblock(c.result, constraint->location); + return true; + } + auto [argsHead, argsTail] = flatten(argsPack); bool blocked = false; @@ -1258,7 +1271,7 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull(subjectType) || get(subjectType)) return block(subjectType, constraint); - auto [blocked, result] = lookupTableProp(subjectType, c.prop, c.context, c.suppressSimplification); + auto [blocked, result] = lookupTableProp(subjectType, c.prop, c.context, c.inConditional, c.suppressSimplification); if (!blocked.empty()) { for (TypeId blocked : blocked) @@ -1432,6 +1445,186 @@ bool ConstraintSolver::tryDispatch(const SetPropConstraint& c, NotNull constraint, TypeId subjectType, TypeId indexType, TypeId resultType) +{ + RecursionLimiter _rl{&recursionDepth, FInt::LuauSolverRecursionLimit}; + + subjectType = follow(subjectType); + indexType = follow(indexType); + + LUAU_ASSERT(get(resultType)); + + if (auto ft = get(subjectType)) + { + FreeType freeResult{ft->scope, builtinTypes->neverType, builtinTypes->unknownType}; + asMutable(resultType)->ty.emplace(freeResult); + + TypeId upperBound = arena->addType(TableType{ + /* props */ {}, + TableIndexer{indexType, resultType}, + TypeLevel{}, + TableState::Unsealed + }); + + unify(constraint, subjectType, upperBound); + + return true; + } + else if (auto tt = getMutable(subjectType)) + { + if (auto indexer = tt->indexer) + { + unify(constraint, indexType, indexer->indexType); + + LUAU_ASSERT(get(resultType)); + bindBlockedType(resultType, indexer->indexResultType, subjectType, constraint->location); + return true; + } + else if (tt->state == TableState::Unsealed) + { + // FIXME this is greedy. + + FreeType freeResult{tt->scope, builtinTypes->neverType, builtinTypes->unknownType}; + asMutable(resultType)->ty.emplace(freeResult); + + tt->indexer = TableIndexer{indexType, resultType}; + return true; + } + } + else if (auto mt = get(subjectType)) + return tryDispatchHasIndexer(recursionDepth, constraint, mt->table, indexType, resultType); + else if (auto ct = get(subjectType)) + { + if (auto indexer = ct->indexer) + { + unify(constraint, indexType, indexer->indexType); + asMutable(resultType)->ty.emplace(indexer->indexResultType); + return true; + } + else if (isString(indexType)) + { + asMutable(resultType)->ty.emplace(builtinTypes->unknownType); + return true; + } + } + else if (auto it = get(subjectType)) + { + // subjectType <: {[indexType]: resultType} + // + // 'a & ~(false | nil) <: {[indexType]: resultType} + // + // 'a <: {[indexType]: resultType} + // ~(false | nil) <: {[indexType]: resultType} + + Set parts{nullptr}; + for (TypeId part: it) + parts.insert(follow(part)); + + Set results{nullptr}; + + for (TypeId part: parts) + { + TypeId r = arena->addType(BlockedType{}); + + bool ok = tryDispatchHasIndexer(recursionDepth, constraint, part, indexType, r); + // FIXME: It's too late to stop and block now I think? We should + // scan for blocked types before we actually do anything. + LUAU_ASSERT(ok); + + r = follow(r); + if (!get(r)) + results.insert(r); + } + + if (0 == results.size()) + asMutable(resultType)->ty.emplace(builtinTypes->errorType); + else if (1 == results.size()) + asMutable(resultType)->ty.emplace(*results.begin()); + else + asMutable(resultType)->ty.emplace(std::vector(results.begin(), results.end())); + + return true; + } + else if (auto ut = get(subjectType)) + { + Set parts{nullptr}; + for (TypeId part: ut) + parts.insert(follow(part)); + + Set results{nullptr}; + + for (TypeId part: parts) + { + TypeId r = arena->addType(BlockedType{}); + + bool ok = tryDispatchHasIndexer(recursionDepth, constraint, part, indexType, r); + // We should have found all the blocked types ahead of time (see BlockedTypeFinder below) + LUAU_ASSERT(ok); + + r = follow(r); + if (!get(r)) + results.insert(r); + } + + if (0 == results.size()) + asMutable(resultType)->ty.emplace(builtinTypes->errorType); + else if (1 == results.size()) + asMutable(resultType)->ty.emplace(*results.begin()); + else + asMutable(resultType)->ty.emplace(std::vector(results.begin(), results.end())); + + return true; + } + + bindBlockedType(resultType, builtinTypes->errorType, subjectType, constraint->location); + + return true; +} + +namespace +{ + +struct BlockedTypeFinder : TypeOnceVisitor +{ + std::optional blocked; + + bool visit(TypeId ty) override + { + // If we've already found one, stop traversing. + return !blocked.has_value(); + } + + bool visit(TypeId ty, const BlockedType&) override + { + blocked = ty; + return false; + } +}; + +} + +bool ConstraintSolver::tryDispatch(const HasIndexerConstraint& c, NotNull constraint) +{ + const TypeId subjectType = follow(c.subjectType); + const TypeId indexType = follow(c.indexType); + + if (isBlocked(subjectType)) + return block(subjectType, constraint); + + if (isBlocked(indexType)) + return block(indexType, constraint); + + BlockedTypeFinder btf; + + btf.visit(subjectType); + + if (btf.blocked) + return block(*btf.blocked, constraint); + int recursionDepth = 0; + + return tryDispatchHasIndexer(recursionDepth, constraint, subjectType, indexType, c.resultType); +} + bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNull constraint, bool force) { TypeId subjectType = follow(c.subjectType); @@ -1456,6 +1649,9 @@ bool ConstraintSolver::tryDispatch(const SetIndexerConstraint& c, NotNullindexer) { + if (isBlocked(tt->indexer->indexResultType)) + return block(tt->indexer->indexResultType, constraint); + // TODO This probably has to be invariant. unify(constraint, c.indexType, tt->indexer->indexType); asMutable(c.propType)->ty.emplace(tt->indexer->indexResultType); @@ -1530,6 +1726,7 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull(resultTy); c.resultIsLValue && lt) { lt->domain = simplifyUnion(builtinTypes, arena, lt->domain, srcTy).result; @@ -1899,14 +2096,14 @@ bool ConstraintSolver::tryDispatchIterableFunction( } std::pair, std::optional> ConstraintSolver::lookupTableProp( - TypeId subjectType, const std::string& propName, ValueContext context, bool suppressSimplification) + TypeId subjectType, const std::string& propName, ValueContext context, bool inConditional, bool suppressSimplification) { DenseHashSet seen{nullptr}; - return lookupTableProp(subjectType, propName, context, suppressSimplification, seen); + return lookupTableProp(subjectType, propName, context, inConditional, suppressSimplification, seen); } std::pair, std::optional> ConstraintSolver::lookupTableProp( - TypeId subjectType, const std::string& propName, ValueContext context, bool suppressSimplification, DenseHashSet& seen) + TypeId subjectType, const std::string& propName, ValueContext context, bool inConditional, bool suppressSimplification, DenseHashSet& seen) { if (seen.contains(subjectType)) return {}; @@ -1971,10 +2168,16 @@ std::pair, std::optional> ConstraintSolver::lookupTa } return {{}, result}; } + + // if we are in a conditional context, we treat the property as present and `unknown` because + // we may be _refining_ a table 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. + if (inConditional) + return {{}, builtinTypes->unknownType}; } else if (auto mt = get(subjectType); mt && context == ValueContext::RValue) { - auto [blocked, result] = lookupTableProp(mt->table, propName, context, suppressSimplification, seen); + auto [blocked, result] = lookupTableProp(mt->table, propName, context, inConditional, suppressSimplification, seen); if (!blocked.empty() || result) return {blocked, result}; @@ -2005,10 +2208,10 @@ std::pair, std::optional> ConstraintSolver::lookupTa } } else - return lookupTableProp(indexType, propName, context, suppressSimplification, seen); + return lookupTableProp(indexType, propName, context, inConditional, suppressSimplification, seen); } else if (get(mtt)) - return lookupTableProp(mtt, propName, context, suppressSimplification, seen); + return lookupTableProp(mtt, propName, context, inConditional, suppressSimplification, seen); } else if (auto ct = get(subjectType)) { @@ -2028,14 +2231,14 @@ std::pair, std::optional> ConstraintSolver::lookupTa if (indexProp == metatable->props.end()) return {{}, std::nullopt}; - return lookupTableProp(indexProp->second.type(), propName, context, suppressSimplification, seen); + return lookupTableProp(indexProp->second.type(), propName, context, inConditional, suppressSimplification, seen); } else if (auto ft = get(subjectType)) { const TypeId upperBound = follow(ft->upperBound); if (get(upperBound) || get(upperBound)) - return lookupTableProp(upperBound, propName, context, suppressSimplification, seen); + return lookupTableProp(upperBound, propName, context, inConditional, suppressSimplification, seen); // TODO: The upper bound could be an intersection that contains suitable tables or classes. @@ -2067,7 +2270,7 @@ std::pair, std::optional> ConstraintSolver::lookupTa for (TypeId ty : utv) { - auto [innerBlocked, innerResult] = lookupTableProp(ty, propName, context, suppressSimplification, seen); + auto [innerBlocked, innerResult] = lookupTableProp(ty, propName, context, inConditional, suppressSimplification, seen); blocked.insert(blocked.end(), innerBlocked.begin(), innerBlocked.end()); if (innerResult) options.insert(*innerResult); @@ -2096,7 +2299,7 @@ std::pair, std::optional> ConstraintSolver::lookupTa for (TypeId ty : itv) { - auto [innerBlocked, innerResult] = lookupTableProp(ty, propName, context, suppressSimplification, seen); + auto [innerBlocked, innerResult] = lookupTableProp(ty, propName, context, inConditional, suppressSimplification, seen); blocked.insert(blocked.end(), innerBlocked.begin(), innerBlocked.end()); if (innerResult) options.insert(*innerResult); @@ -2118,6 +2321,14 @@ std::pair, std::optional> ConstraintSolver::lookupTa else return {{}, arena->addType(IntersectionType{std::vector(begin(options), end(options))})}; } + else if (auto pt = get(subjectType)) + { + // if we are in a conditional context, we treat the property as present and `unknown` because + // we may be _refining_ a table 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. + if (inConditional && pt->type == PrimitiveType::Table) + return {{}, builtinTypes->unknownType}; + } return {{}, std::nullopt}; } diff --git a/Analysis/src/DataFlowGraph.cpp b/Analysis/src/DataFlowGraph.cpp index c82ec9c96..b67614c49 100644 --- a/Analysis/src/DataFlowGraph.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -251,8 +251,12 @@ void DataFlowGraphBuilder::joinProps(DfgScope* result, const DfgScope& a, const DefId DataFlowGraphBuilder::lookup(DfgScope* scope, Symbol symbol) { + // true if any of the considered scopes are a loop. + bool outsideLoopScope = false; for (DfgScope* current = scope; current; current = current->parent) { + outsideLoopScope = outsideLoopScope || current->scopeType == DfgScope::Loop; + if (auto found = current->bindings.find(symbol)) return NotNull{*found}; else if (current->scopeType == DfgScope::Function) @@ -260,7 +264,12 @@ DefId DataFlowGraphBuilder::lookup(DfgScope* scope, Symbol symbol) FunctionCapture& capture = captures[symbol]; DefId captureDef = defArena->phi({}); capture.captureDefs.push_back(captureDef); - scope->bindings[symbol] = captureDef; + + // If we are outside of a loop scope, then we don't want to actually bind + // uses of `symbol` to this new phi node since it will not get populated. + if (!outsideLoopScope) + scope->bindings[symbol] = captureDef; + return NotNull{captureDef}; } } diff --git a/Analysis/src/OverloadResolution.cpp b/Analysis/src/OverloadResolution.cpp index c6c71baaf..556941249 100644 --- a/Analysis/src/OverloadResolution.cpp +++ b/Analysis/src/OverloadResolution.cpp @@ -292,8 +292,13 @@ std::pair OverloadResolver::checkOverload_ if (failedSubPack && failedSuperPack) { - LUAU_ASSERT(!argExprs->empty()); - argLocation = argExprs->at(argExprs->size() - 1)->location; + // If a bug in type inference occurs, we may have a mismatch in the return packs. + // This happens when inference incorrectly leaves the result type of a function free. + // If this happens, we don't want to explode, so we'll use the function's location. + if (argExprs->empty()) + argLocation = fnExpr->location; + else + argLocation = argExprs->at(argExprs->size() - 1)->location; // TODO extract location from the SubtypingResult path and argExprs switch (reason.variance) diff --git a/Analysis/src/Set.cpp b/Analysis/src/Set.cpp new file mode 100644 index 000000000..1819e28a1 --- /dev/null +++ b/Analysis/src/Set.cpp @@ -0,0 +1,5 @@ +// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details + +#include "Luau/Common.h" + +LUAU_FASTFLAGVARIABLE(LuauFixSetIter, false) diff --git a/Analysis/src/Simplify.cpp b/Analysis/src/Simplify.cpp index 931ec6d70..a21f50929 100644 --- a/Analysis/src/Simplify.cpp +++ b/Analysis/src/Simplify.cpp @@ -13,6 +13,7 @@ LUAU_FASTINT(LuauTypeReductionRecursionLimit) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) +LUAU_DYNAMIC_FASTINTVARIABLE(LuauSimplificationComplexityLimit, 8); namespace Luau { @@ -237,16 +238,12 @@ Relation relateTables(TypeId left, TypeId right, SimplifierSeenSet& seen) LUAU_ASSERT(1 == rightTable->props.size()); // Disjoint props have nothing in common // t1 with props p1's cannot appear in t2 and t2 with props p2's cannot appear in t1 - bool foundPropFromLeftInRight = std::any_of(begin(leftTable->props), end(leftTable->props), - [&](auto prop) - { - return rightTable->props.count(prop.first) > 0; - }); - bool foundPropFromRightInLeft = std::any_of(begin(rightTable->props), end(rightTable->props), - [&](auto prop) - { - return leftTable->props.count(prop.first) > 0; - }); + bool foundPropFromLeftInRight = std::any_of(begin(leftTable->props), end(leftTable->props), [&](auto prop) { + return rightTable->props.count(prop.first) > 0; + }); + bool foundPropFromRightInLeft = std::any_of(begin(rightTable->props), end(rightTable->props), [&](auto prop) { + return leftTable->props.count(prop.first) > 0; + }); if (!foundPropFromLeftInRight && !foundPropFromRightInLeft && leftTable->props.size() >= 1 && rightTable->props.size() >= 1) return Relation::Disjoint; @@ -689,6 +686,9 @@ TypeId TypeSimplifier::intersectUnionWithType(TypeId left, TypeId right) bool changed = false; std::set newParts; + if (leftUnion->options.size() > (size_t)DFInt::LuauSimplificationComplexityLimit) + return arena->addType(IntersectionType{{left, right}}); + for (TypeId part : leftUnion) { TypeId simplified = intersect(right, part); @@ -723,6 +723,15 @@ TypeId TypeSimplifier::intersectUnions(TypeId left, TypeId right) std::set newParts; + // Combinatorial blowup moment!! + + // combination size + size_t optionSize = (int)leftUnion->options.size() * rightUnion->options.size(); + size_t maxSize = DFInt::LuauSimplificationComplexityLimit; + + if (optionSize > maxSize) + return arena->addType(IntersectionType{{left, right}}); + for (TypeId leftPart : leftUnion) { for (TypeId rightPart : rightUnion) @@ -986,6 +995,9 @@ TypeId TypeSimplifier::intersectIntersectionWithType(TypeId left, TypeId right) const IntersectionType* leftIntersection = get(left); LUAU_ASSERT(leftIntersection); + if (leftIntersection->parts.size() > (size_t)DFInt::LuauSimplificationComplexityLimit) + return arena->addType(IntersectionType{{left, right}}); + bool changed = false; std::set newParts; @@ -1135,6 +1147,10 @@ TypeId TypeSimplifier::intersect(TypeId left, TypeId right) return right; if (get(right) && get(left)) return left; + if (get(left) && !get(right)) + return right; + if (get(right) && !get(left)) + return left; if (get(left)) return arena->addType(UnionType{{right, builtinTypes->errorType}}); if (get(right)) diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index 8a8b668a0..a894d97b1 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -1615,7 +1615,6 @@ std::pair Subtyping::handleTypeFamilyReductionResult(const Typ { TypeFamilyContext context{arena, builtinTypes, scope, normalizer, iceReporter, NotNull{&limits}}; TypeId family = arena->addType(*familyInstance); - std::string familyString = toString(family); FamilyGraphReductionResult result = reduceFamilies(family, {}, context, true); ErrorVec errors; if (result.blockedTypes.size() != 0 || result.blockedPacks.size() != 0) diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index c4cccc8f5..9bb2d065f 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -1776,6 +1776,10 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts) const std::string pathStr = c.path.size() == 1 ? "\"" + c.path[0] + "\"" : "[\"" + join(c.path, "\", \"") + "\"]"; return tos(c.resultType) + " ~ setProp " + tos(c.subjectType) + ", " + pathStr + " " + tos(c.propType); } + else if constexpr (std::is_same_v) + { + return tos(c.resultType) + " ~ hasIndexer " + tos(c.subjectType) + " " + tos(c.indexType); + } else if constexpr (std::is_same_v) { return tos(c.resultType) + " ~ setIndexer " + tos(c.subjectType) + " [ " + tos(c.indexType) + " ] " + tos(c.propType); diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index cbeaa1f3a..61475a579 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -242,6 +242,7 @@ struct TypeChecker2 Module* module; TypeArena testArena; + TypeContext typeContext = TypeContext::Default; std::vector> stack; std::vector functionDeclStack; @@ -619,7 +620,11 @@ struct TypeChecker2 void visit(AstStatIf* ifStatement) { - visit(ifStatement->condition, ValueContext::RValue); + { + InConditionalContext flipper{&typeContext}; + visit(ifStatement->condition, ValueContext::RValue); + } + visit(ifStatement->thenbody); if (ifStatement->elsebody) visit(ifStatement->elsebody); @@ -1254,20 +1259,8 @@ struct TypeChecker2 if (!originalCallTy) return; - TypeId fnTy = *originalCallTy; - if (selectedOverloadTy) - { - SubtypingResult result = subtyping->isSubtype(*originalCallTy, *selectedOverloadTy); - if (result.isSubtype) - fnTy = *selectedOverloadTy; + TypeId fnTy = follow(*originalCallTy); - if (result.normalizationTooComplex) - { - reportError(NormalizationTooComplex{}, call->func->location); - return; - } - } - fnTy = follow(fnTy); if (get(fnTy) || get(fnTy) || get(fnTy)) return; @@ -1286,6 +1279,19 @@ struct TypeChecker2 return; } + if (selectedOverloadTy) + { + SubtypingResult result = subtyping->isSubtype(*originalCallTy, *selectedOverloadTy); + if (result.isSubtype) + fnTy = follow(*selectedOverloadTy); + + if (result.normalizationTooComplex) + { + reportError(NormalizationTooComplex{}, call->func->location); + return; + } + } + if (call->self) { AstExprIndexName* indexExpr = call->func->as(); @@ -1323,6 +1329,8 @@ struct TypeChecker2 args.head.push_back(builtinTypes->anyType); } + + OverloadResolver resolver{ builtinTypes, NotNull{&testArena}, @@ -1332,8 +1340,8 @@ struct TypeChecker2 limits, call->location, }; - resolver.resolve(fnTy, &args, call->func, &argExprs); + auto norm = normalizer.normalize(fnTy); if (!norm) reportError(NormalizationTooComplex{}, call->func->location); @@ -1505,8 +1513,13 @@ struct TypeChecker2 else reportError(CannotExtendTable{exprType, CannotExtendTable::Indexer, "indexer??"}, indexExpr->location); } - else if (auto cls = get(exprType); cls && cls->indexer) - testIsSubtype(indexType, cls->indexer->indexType, indexExpr->index->location); + else if (auto cls = get(exprType)) + { + if (cls->indexer) + testIsSubtype(indexType, cls->indexer->indexType, indexExpr->index->location); + else + reportError(DynamicPropertyLookupOnClassesUnsafe{exprType}, indexExpr->location); + } else if (get(exprType) && isOptional(exprType)) { switch (shouldSuppressErrors(NotNull{&normalizer}, exprType)) @@ -2710,7 +2723,11 @@ struct TypeChecker2 return true; } - return false; + + // 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); } else if (const ClassType* cls = get(ty)) { @@ -2735,6 +2752,8 @@ struct TypeChecker2 return std::any_of(begin(itv), end(itv), [&](TypeId part) { return hasIndexTypeFromType(part, prop, context, location, seen, astIndexExprType, errors); }); + else if (const PrimitiveType* pt = get(ty)) + return inConditional(typeContext) && pt->type == PrimitiveType::Table; else return false; } diff --git a/Analysis/src/TypeUtils.cpp b/Analysis/src/TypeUtils.cpp index 795324f47..6a03039ae 100644 --- a/Analysis/src/TypeUtils.cpp +++ b/Analysis/src/TypeUtils.cpp @@ -14,6 +14,11 @@ LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution); namespace Luau { +bool inConditional(const TypeContext& context) +{ + return context == TypeContext::Condition; +} + std::optional findMetatableEntry( NotNull builtinTypes, ErrorVec& errors, TypeId type, const std::string& entry, Location location) { diff --git a/CodeGen/include/Luau/CodeGen.h b/CodeGen/include/Luau/CodeGen.h index 965aa7c24..9e51c54ad 100644 --- a/CodeGen/include/Luau/CodeGen.h +++ b/CodeGen/include/Luau/CodeGen.h @@ -58,6 +58,12 @@ bool isSupported(); void create(lua_State* L, AllocationCallback* allocationCallback, void* allocationCallbackContext); void create(lua_State* L); +// Check if native execution is enabled +[[nodiscard]] bool isNativeExecutionEnabled(lua_State* L); + +// Enable or disable native execution according to `enabled` argument +void setNativeExecutionEnabled(lua_State* L, bool enabled); + // Builds target function and all inner functions CodeGenCompilationResult compile(lua_State* L, int idx, unsigned int flags = 0, CompilationStats* stats = nullptr); diff --git a/CodeGen/include/Luau/IrData.h b/CodeGen/include/Luau/IrData.h index a950370b9..7cefec7b8 100644 --- a/CodeGen/include/Luau/IrData.h +++ b/CodeGen/include/Luau/IrData.h @@ -322,7 +322,7 @@ enum class IrCmd : uint8_t ADJUST_STACK_TO_TOP, // Execute fastcall builtin function in-place - // A: builtin + // A: unsigned int (builtin id) // B: Rn (result start) // C: Rn (argument start) // D: Rn or Kn or undef (optional second argument) @@ -331,7 +331,7 @@ enum class IrCmd : uint8_t FASTCALL, // Call the fastcall builtin function - // A: builtin + // A: unsigned int (builtin id) // B: Rn (result start) // C: Rn (argument start) // D: Rn or Kn or undef (optional second argument) diff --git a/CodeGen/include/Luau/IrVisitUseDef.h b/CodeGen/include/Luau/IrVisitUseDef.h index 27244cb86..3d67d13ce 100644 --- a/CodeGen/include/Luau/IrVisitUseDef.h +++ b/CodeGen/include/Luau/IrVisitUseDef.h @@ -4,7 +4,7 @@ #include "Luau/Common.h" #include "Luau/IrData.h" -LUAU_FASTFLAG(LuauCodegenRemoveDeadStores2) +LUAU_FASTFLAG(LuauCodegenRemoveDeadStores3) namespace Luau { @@ -188,7 +188,7 @@ static void visitVmRegDefsUses(T& visitor, IrFunction& function, const IrInst& i visitor.def(inst.b); break; case IrCmd::FALLBACK_FORGPREP: - if (FFlag::LuauCodegenRemoveDeadStores2) + if (FFlag::LuauCodegenRemoveDeadStores3) { // This instruction doesn't always redefine Rn, Rn+1, Rn+2, so we have to mark it as implicit use visitor.useRange(vmRegOp(inst.b), 3); diff --git a/CodeGen/src/AssemblyBuilderA64.cpp b/CodeGen/src/AssemblyBuilderA64.cpp index 9d0522c01..85d34db88 100644 --- a/CodeGen/src/AssemblyBuilderA64.cpp +++ b/CodeGen/src/AssemblyBuilderA64.cpp @@ -7,6 +7,8 @@ #include #include +LUAU_FASTFLAG(LuauCodeGenOptVecA64) + namespace Luau { namespace CodeGen @@ -557,25 +559,41 @@ void AssemblyBuilderA64::fmov(RegisterA64 dst, RegisterA64 src) void AssemblyBuilderA64::fmov(RegisterA64 dst, double src) { - CODEGEN_ASSERT(dst.kind == KindA64::d || dst.kind == KindA64::q); + if (FFlag::LuauCodeGenOptVecA64) + { + CODEGEN_ASSERT(dst.kind == KindA64::d || dst.kind == KindA64::q); - int imm = getFmovImm(src); - CODEGEN_ASSERT(imm >= 0 && imm <= 256); + int imm = getFmovImm(src); + CODEGEN_ASSERT(imm >= 0 && imm <= 256); - // fmov can't encode 0, but movi can; movi is otherwise not useful for fp immediates because it encodes repeating patterns - if (dst.kind == KindA64::d) - { - if (imm == 256) - placeFMOV("movi", dst, src, 0b001'0111100000'000'1110'01'00000); + // fmov can't encode 0, but movi can; movi is otherwise not useful for fp immediates because it encodes repeating patterns + if (dst.kind == KindA64::d) + { + if (imm == 256) + placeFMOV("movi", dst, src, 0b001'0111100000'000'1110'01'00000); + else + placeFMOV("fmov", dst, src, 0b000'11110'01'1'00000000'100'00000 | (imm << 8)); + } else - placeFMOV("fmov", dst, src, 0b000'11110'01'1'00000000'100'00000 | (imm << 8)); + { + if (imm == 256) + placeFMOV("movi.4s", dst, src, 0b010'0111100000'000'0000'01'00000); + else + placeFMOV("fmov.4s", dst, src, 0b010'0111100000'000'1111'0'1'00000 | ((imm >> 5) << 11) | (imm & 31)); + } } else { + CODEGEN_ASSERT(dst.kind == KindA64::d); + + int imm = getFmovImm(src); + CODEGEN_ASSERT(imm >= 0 && imm <= 256); + + // fmov can't encode 0, but movi can; movi is otherwise not useful for 64-bit fp immediates because it encodes repeating patterns if (imm == 256) - placeFMOV("movi.4s", dst, src, 0b010'0111100000'000'0000'01'00000); + placeFMOV("movi", dst, src, 0b001'0111100000'000'1110'01'00000); else - placeFMOV("fmov.4s", dst, src, 0b010'0111100000'000'1111'0'1'00000 | ((imm >> 5) << 11) | (imm & 31)); + placeFMOV("fmov", dst, src, 0b000'11110'01'1'00000000'100'00000 | (imm << 8)); } } diff --git a/CodeGen/src/CodeGen.cpp b/CodeGen/src/CodeGen.cpp index bf500d691..14f5b5b58 100644 --- a/CodeGen/src/CodeGen.cpp +++ b/CodeGen/src/CodeGen.cpp @@ -57,6 +57,8 @@ LUAU_FASTINTVARIABLE(CodegenHeuristicsBlockLimit, 32'768) // 32 K // Current value is based on some member variables being limited to 16 bits LUAU_FASTINTVARIABLE(CodegenHeuristicsBlockInstructionLimit, 65'536) // 64 K +LUAU_FASTFLAG(LuauCodegenHeapSizeReport) + namespace Luau { namespace CodeGen @@ -74,25 +76,94 @@ struct NativeProto uintptr_t exectarget; }; -static NativeProto createNativeProto(Proto* proto, const IrBuilder& ir) +// Additional data attached to Proto::execdata +// Guaranteed to be aligned to 16 bytes +struct ExtraExecData +{ + size_t execDataSize; + size_t codeSize; +}; + +static int alignTo(int value, int align) +{ + CODEGEN_ASSERT(FFlag::LuauCodegenHeapSizeReport); + CODEGEN_ASSERT(align > 0 && (align & (align - 1)) == 0); + return (value + (align - 1)) & ~(align - 1); +} + +// Returns the size of execdata required to store all code offsets and ExtraExecData structure at proper alignment +// Always a multiple of 4 bytes +static int calculateExecDataSize(Proto* proto) { - int sizecode = proto->sizecode; + CODEGEN_ASSERT(FFlag::LuauCodegenHeapSizeReport); + int size = proto->sizecode * sizeof(uint32_t); + + size = alignTo(size, 16); + size += sizeof(ExtraExecData); + + return size; +} + +// Returns pointer to the ExtraExecData inside the Proto::execdata +// Even though 'execdata' is a field in Proto, we require it to support cases where it's not attached to Proto during construction +ExtraExecData* getExtraExecData(Proto* proto, void* execdata) +{ + CODEGEN_ASSERT(FFlag::LuauCodegenHeapSizeReport); + int size = proto->sizecode * sizeof(uint32_t); + + size = alignTo(size, 16); - uint32_t* instOffsets = new uint32_t[sizecode]; - uint32_t instTarget = ir.function.entryLocation; + return reinterpret_cast(reinterpret_cast(execdata) + size); +} - for (int i = 0; i < sizecode; i++) +static NativeProto createNativeProto(Proto* proto, const IrBuilder& ir) +{ + if (FFlag::LuauCodegenHeapSizeReport) { - CODEGEN_ASSERT(ir.function.bcMapping[i].asmLocation >= instTarget); + int execDataSize = calculateExecDataSize(proto); + CODEGEN_ASSERT(execDataSize % 4 == 0); + + uint32_t* execData = new uint32_t[execDataSize / 4]; + uint32_t instTarget = ir.function.entryLocation; + + for (int i = 0; i < proto->sizecode; i++) + { + CODEGEN_ASSERT(ir.function.bcMapping[i].asmLocation >= instTarget); + + execData[i] = ir.function.bcMapping[i].asmLocation - instTarget; + } + + // Set first instruction offset to 0 so that entering this function still executes any generated entry code. + execData[0] = 0; + + ExtraExecData* extra = getExtraExecData(proto, execData); + memset(extra, 0, sizeof(ExtraExecData)); + + extra->execDataSize = execDataSize; - instOffsets[i] = ir.function.bcMapping[i].asmLocation - instTarget; + // entry target will be relocated when assembly is finalized + return {proto, execData, instTarget}; } + else + { + int sizecode = proto->sizecode; + + uint32_t* instOffsets = new uint32_t[sizecode]; + uint32_t instTarget = ir.function.entryLocation; - // Set first instruction offset to 0 so that entering this function still executes any generated entry code. - instOffsets[0] = 0; + for (int i = 0; i < sizecode; i++) + { + CODEGEN_ASSERT(ir.function.bcMapping[i].asmLocation >= instTarget); - // entry target will be relocated when assembly is finalized - return {proto, instOffsets, instTarget}; + instOffsets[i] = ir.function.bcMapping[i].asmLocation - instTarget; + } + + // Set first instruction offset to 0 so that entering this function still executes any generated entry code. + instOffsets[0] = 0; + + // entry target will be relocated when assembly is finalized + return {proto, instOffsets, instTarget}; + } } static void destroyExecData(void* execdata) @@ -168,6 +239,12 @@ static int onEnter(lua_State* L, Proto* proto) return GateFn(data->context.gateEntry)(L, proto, target, &data->context); } +// used to disable native execution, unconditionally +static int onEnterDisabled(lua_State* L, Proto* proto) +{ + return 1; +} + void onDisable(lua_State* L, Proto* proto) { // do nothing if proto already uses bytecode @@ -207,6 +284,17 @@ void onDisable(lua_State* L, Proto* proto) }); } +size_t getMemorySize(lua_State* L, Proto* proto) +{ + CODEGEN_ASSERT(FFlag::LuauCodegenHeapSizeReport); + ExtraExecData* extra = getExtraExecData(proto, proto->execdata); + + // While execDataSize is exactly the size of the allocation we made and hold for 'execdata' field, the code size is approximate + // This is because code+data page is shared and owned by all Proto from a single module and each one can keep the whole region alive + // So individual Proto being freed by GC will not reflect memory use by native code correctly + return extra->execDataSize + extra->codeSize; +} + #if defined(__aarch64__) unsigned int getCpuFeaturesA64() { @@ -301,6 +389,9 @@ void create(lua_State* L, AllocationCallback* allocationCallback, void* allocati ecb->destroy = onDestroyFunction; ecb->enter = onEnter; ecb->disable = onDisable; + + if (FFlag::LuauCodegenHeapSizeReport) + ecb->getmemorysize = getMemorySize; } void create(lua_State* L) @@ -308,6 +399,17 @@ void create(lua_State* L) create(L, nullptr, nullptr); } +[[nodiscard]] bool isNativeExecutionEnabled(lua_State* L) +{ + return getNativeState(L) ? (L->global->ecb.enter == onEnter) : false; +} + +void setNativeExecutionEnabled(lua_State* L, bool enabled) +{ + if (getNativeState(L)) + L->global->ecb.enter = enabled ? onEnter : onEnterDisabled; +} + CodeGenCompilationResult compile(lua_State* L, int idx, unsigned int flags, CompilationStats* stats) { CODEGEN_ASSERT(lua_isLfunction(L, idx)); @@ -401,9 +503,10 @@ CodeGenCompilationResult compile(lua_State* L, int idx, unsigned int flags, Comp return CodeGenCompilationResult::AllocationFailed; } - if (gPerfLogFn && results.size() > 0) + if (FFlag::LuauCodegenHeapSizeReport) { - gPerfLogFn(gPerfLogContext, uintptr_t(codeStart), uint32_t(results[0].exectarget), ""); + if (gPerfLogFn && results.size() > 0) + gPerfLogFn(gPerfLogContext, uintptr_t(codeStart), uint32_t(results[0].exectarget), ""); for (size_t i = 0; i < results.size(); ++i) { @@ -411,7 +514,27 @@ CodeGenCompilationResult compile(lua_State* L, int idx, unsigned int flags, Comp uint32_t end = i + 1 < results.size() ? uint32_t(results[i + 1].exectarget) : uint32_t(build.code.size() * sizeof(build.code[0])); CODEGEN_ASSERT(begin < end); - logPerfFunction(results[i].p, uintptr_t(codeStart) + begin, end - begin); + if (gPerfLogFn) + logPerfFunction(results[i].p, uintptr_t(codeStart) + begin, end - begin); + + ExtraExecData* extra = getExtraExecData(results[i].p, results[i].execdata); + extra->codeSize = end - begin; + } + } + else + { + if (gPerfLogFn && results.size() > 0) + { + gPerfLogFn(gPerfLogContext, uintptr_t(codeStart), uint32_t(results[0].exectarget), ""); + + for (size_t i = 0; i < results.size(); ++i) + { + uint32_t begin = uint32_t(results[i].exectarget); + uint32_t end = i + 1 < results.size() ? uint32_t(results[i + 1].exectarget) : uint32_t(build.code.size() * sizeof(build.code[0])); + CODEGEN_ASSERT(begin < end); + + logPerfFunction(results[i].p, uintptr_t(codeStart) + begin, end - begin); + } } } diff --git a/CodeGen/src/CodeGenLower.h b/CodeGen/src/CodeGenLower.h index 33f395ff1..4f7691783 100644 --- a/CodeGen/src/CodeGenLower.h +++ b/CodeGen/src/CodeGenLower.h @@ -27,7 +27,7 @@ LUAU_FASTFLAG(DebugCodegenSkipNumbering) LUAU_FASTINT(CodegenHeuristicsInstructionLimit) LUAU_FASTINT(CodegenHeuristicsBlockLimit) LUAU_FASTINT(CodegenHeuristicsBlockInstructionLimit) -LUAU_FASTFLAG(LuauCodegenRemoveDeadStores2) +LUAU_FASTFLAG(LuauCodegenRemoveDeadStores3) namespace Luau { @@ -312,7 +312,7 @@ inline bool lowerFunction(IrBuilder& ir, AssemblyBuilder& build, ModuleHelpers& } } - if (FFlag::LuauCodegenRemoveDeadStores2) + if (FFlag::LuauCodegenRemoveDeadStores3) markDeadStoresInBlockChains(ir); } diff --git a/CodeGen/src/EmitBuiltinsX64.cpp b/CodeGen/src/EmitBuiltinsX64.cpp index d20de4311..fdce733fa 100644 --- a/CodeGen/src/EmitBuiltinsX64.cpp +++ b/CodeGen/src/EmitBuiltinsX64.cpp @@ -12,6 +12,8 @@ #include "lstate.h" +LUAU_FASTFLAG(LuauCodegenRemoveDeadStores3) + namespace Luau { namespace CodeGen @@ -28,10 +30,16 @@ static void emitBuiltinMathFrexp(IrRegAllocX64& regs, AssemblyBuilderX64& build, build.vmovsd(luauRegValue(ra), xmm0); + if (FFlag::LuauCodegenRemoveDeadStores3) + build.mov(luauRegTag(ra), LUA_TNUMBER); + if (nresults > 1) { build.vcvtsi2sd(xmm0, xmm0, dword[sTemporarySlot + 0]); build.vmovsd(luauRegValue(ra + 1), xmm0); + + if (FFlag::LuauCodegenRemoveDeadStores3) + build.mov(luauRegTag(ra + 1), LUA_TNUMBER); } } @@ -45,8 +53,16 @@ static void emitBuiltinMathModf(IrRegAllocX64& regs, AssemblyBuilderX64& build, build.vmovsd(xmm1, qword[sTemporarySlot + 0]); build.vmovsd(luauRegValue(ra), xmm1); + if (FFlag::LuauCodegenRemoveDeadStores3) + build.mov(luauRegTag(ra), LUA_TNUMBER); + if (nresults > 1) + { build.vmovsd(luauRegValue(ra + 1), xmm0); + + if (FFlag::LuauCodegenRemoveDeadStores3) + build.mov(luauRegTag(ra + 1), LUA_TNUMBER); + } } static void emitBuiltinMathSign(IrRegAllocX64& regs, AssemblyBuilderX64& build, int ra, int arg) @@ -74,6 +90,9 @@ static void emitBuiltinMathSign(IrRegAllocX64& regs, AssemblyBuilderX64& build, build.vblendvpd(tmp0.reg, tmp2.reg, build.f64x2(1, 1), tmp0.reg); build.vmovsd(luauRegValue(ra), tmp0.reg); + + if (FFlag::LuauCodegenRemoveDeadStores3) + build.mov(luauRegTag(ra), LUA_TNUMBER); } void emitBuiltin(IrRegAllocX64& regs, AssemblyBuilderX64& build, int bfid, int ra, int arg, OperandX64 arg2, int nparams, int nresults) diff --git a/CodeGen/src/IrLoweringA64.cpp b/CodeGen/src/IrLoweringA64.cpp index 284cef4d4..5f101b9f8 100644 --- a/CodeGen/src/IrLoweringA64.cpp +++ b/CodeGen/src/IrLoweringA64.cpp @@ -15,6 +15,7 @@ LUAU_FASTFLAGVARIABLE(LuauCodeGenVectorA64, false) LUAU_FASTFLAGVARIABLE(LuauCodeGenOptVecA64, false) LUAU_FASTFLAG(LuauCodegenVectorTag2) +LUAU_FASTFLAG(LuauCodegenRemoveDeadStores3) namespace Luau { @@ -202,25 +203,71 @@ static bool emitBuiltin( switch (bfid) { case LBF_MATH_FREXP: - CODEGEN_ASSERT(nparams == 1 && (nresults == 1 || nresults == 2)); - emitInvokeLibm1P(build, offsetof(NativeContext, libm_frexp), arg); - build.str(d0, mem(rBase, res * sizeof(TValue) + offsetof(TValue, value.n))); - if (nresults == 2) + { + if (FFlag::LuauCodegenRemoveDeadStores3) + { + CODEGEN_ASSERT(nparams == 1 && (nresults == 1 || nresults == 2)); + emitInvokeLibm1P(build, offsetof(NativeContext, libm_frexp), arg); + build.str(d0, mem(rBase, res * sizeof(TValue) + offsetof(TValue, value.n))); + + RegisterA64 temp = regs.allocTemp(KindA64::w); + build.mov(temp, LUA_TNUMBER); + build.str(temp, mem(rBase, res * sizeof(TValue) + offsetof(TValue, tt))); + + if (nresults == 2) + { + build.ldr(w0, sTemporary); + build.scvtf(d1, w0); + build.str(d1, mem(rBase, (res + 1) * sizeof(TValue) + offsetof(TValue, value.n))); + build.str(temp, mem(rBase, (res + 1) * sizeof(TValue) + offsetof(TValue, tt))); + } + } + else { - build.ldr(w0, sTemporary); - build.scvtf(d1, w0); - build.str(d1, mem(rBase, (res + 1) * sizeof(TValue) + offsetof(TValue, value.n))); + CODEGEN_ASSERT(nparams == 1 && (nresults == 1 || nresults == 2)); + emitInvokeLibm1P(build, offsetof(NativeContext, libm_frexp), arg); + build.str(d0, mem(rBase, res * sizeof(TValue) + offsetof(TValue, value.n))); + if (nresults == 2) + { + build.ldr(w0, sTemporary); + build.scvtf(d1, w0); + build.str(d1, mem(rBase, (res + 1) * sizeof(TValue) + offsetof(TValue, value.n))); + } } return true; + } case LBF_MATH_MODF: - CODEGEN_ASSERT(nparams == 1 && (nresults == 1 || nresults == 2)); - emitInvokeLibm1P(build, offsetof(NativeContext, libm_modf), arg); - build.ldr(d1, sTemporary); - build.str(d1, mem(rBase, res * sizeof(TValue) + offsetof(TValue, value.n))); - if (nresults == 2) - build.str(d0, mem(rBase, (res + 1) * sizeof(TValue) + offsetof(TValue, value.n))); + { + if (FFlag::LuauCodegenRemoveDeadStores3) + { + CODEGEN_ASSERT(nparams == 1 && (nresults == 1 || nresults == 2)); + emitInvokeLibm1P(build, offsetof(NativeContext, libm_modf), arg); + build.ldr(d1, sTemporary); + build.str(d1, mem(rBase, res * sizeof(TValue) + offsetof(TValue, value.n))); + + RegisterA64 temp = regs.allocTemp(KindA64::w); + build.mov(temp, LUA_TNUMBER); + build.str(temp, mem(rBase, res * sizeof(TValue) + offsetof(TValue, tt))); + + if (nresults == 2) + { + build.str(d0, mem(rBase, (res + 1) * sizeof(TValue) + offsetof(TValue, value.n))); + build.str(temp, mem(rBase, (res + 1) * sizeof(TValue) + offsetof(TValue, tt))); + } + } + else + { + CODEGEN_ASSERT(nparams == 1 && (nresults == 1 || nresults == 2)); + emitInvokeLibm1P(build, offsetof(NativeContext, libm_modf), arg); + build.ldr(d1, sTemporary); + build.str(d1, mem(rBase, res * sizeof(TValue) + offsetof(TValue, value.n))); + if (nresults == 2) + build.str(d0, mem(rBase, (res + 1) * sizeof(TValue) + offsetof(TValue, value.n))); + } return true; + } case LBF_MATH_SIGN: + { CODEGEN_ASSERT(nparams == 1 && nresults == 1); build.ldr(d0, mem(rBase, arg * sizeof(TValue) + offsetof(TValue, value.n))); build.fcmpz(d0); @@ -230,7 +277,15 @@ static bool emitBuiltin( build.fmov(d1, -1.0); build.fcsel(d0, d1, d0, getConditionFP(IrCondition::Less)); build.str(d0, mem(rBase, res * sizeof(TValue) + offsetof(TValue, value.n))); + + if (FFlag::LuauCodegenRemoveDeadStores3) + { + RegisterA64 temp = regs.allocTemp(KindA64::w); + build.mov(temp, LUA_TNUMBER); + build.str(temp, mem(rBase, res * sizeof(TValue) + offsetof(TValue, tt))); + } return true; + } default: CODEGEN_ASSERT(!"Missing A64 lowering"); @@ -1192,7 +1247,7 @@ void IrLoweringA64::lowerInst(IrInst& inst, uint32_t index, const IrBlock& next) { RegisterA64 temp = regs.allocTemp(KindA64::x); - uint32_t vec[4] = { asU32, asU32, asU32, 0 }; + uint32_t vec[4] = {asU32, asU32, asU32, 0}; build.adr(temp, vec, sizeof(vec)); build.ldr(inst.regA64, temp); } diff --git a/CodeGen/src/IrTranslateBuiltins.cpp b/CodeGen/src/IrTranslateBuiltins.cpp index b7630adf1..30778fd97 100644 --- a/CodeGen/src/IrTranslateBuiltins.cpp +++ b/CodeGen/src/IrTranslateBuiltins.cpp @@ -8,6 +8,8 @@ #include +LUAU_FASTFLAG(LuauCodegenRemoveDeadStores3) + // TODO: when nresults is less than our actual result count, we can skip computing/writing unused results static const int kMinMaxUnrolledParams = 5; @@ -46,8 +48,11 @@ static BuiltinImplResult translateBuiltinNumberToNumber( builtinCheckDouble(build, build.vmReg(arg), pcpos); build.inst(IrCmd::FASTCALL, build.constUint(bfid), build.vmReg(ra), build.vmReg(arg), args, build.constInt(1), build.constInt(1)); - if (ra != arg) - build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER)); + if (!FFlag::LuauCodegenRemoveDeadStores3) + { + if (ra != arg) + build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER)); + } return {BuiltinImplType::Full, 1}; } @@ -107,11 +112,14 @@ static BuiltinImplResult translateBuiltinNumberTo2Number( build.inst( IrCmd::FASTCALL, build.constUint(bfid), build.vmReg(ra), build.vmReg(arg), args, build.constInt(1), build.constInt(nresults == 1 ? 1 : 2)); - if (ra != arg) - build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER)); + if (!FFlag::LuauCodegenRemoveDeadStores3) + { + if (ra != arg) + build.inst(IrCmd::STORE_TAG, build.vmReg(ra), build.constTag(LUA_TNUMBER)); - if (nresults != 1) - build.inst(IrCmd::STORE_TAG, build.vmReg(ra + 1), build.constTag(LUA_TNUMBER)); + if (nresults != 1) + build.inst(IrCmd::STORE_TAG, build.vmReg(ra + 1), build.constTag(LUA_TNUMBER)); + } return {BuiltinImplType::Full, 2}; } diff --git a/CodeGen/src/IrValueLocationTracking.cpp b/CodeGen/src/IrValueLocationTracking.cpp index 182dd736c..93a452ffc 100644 --- a/CodeGen/src/IrValueLocationTracking.cpp +++ b/CodeGen/src/IrValueLocationTracking.cpp @@ -3,8 +3,6 @@ #include "Luau/IrUtils.h" -LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauCodegenTrackingMultilocationFix, false) - namespace Luau { namespace CodeGen @@ -161,7 +159,7 @@ void IrValueLocationTracking::afterInstLowering(IrInst& inst, uint32_t instIdx) case IrCmd::LOAD_DOUBLE: case IrCmd::LOAD_INT: case IrCmd::LOAD_TVALUE: - if (DFFlag::LuauCodegenTrackingMultilocationFix && inst.a.kind == IrOpKind::VmReg) + if (inst.a.kind == IrOpKind::VmReg) invalidateRestoreOp(inst.a, /*skipValueInvalidation*/ false); recordRestoreOp(instIdx, inst.a); diff --git a/CodeGen/src/OptimizeConstProp.cpp b/CodeGen/src/OptimizeConstProp.cpp index ff4f7bfc0..946fac416 100644 --- a/CodeGen/src/OptimizeConstProp.cpp +++ b/CodeGen/src/OptimizeConstProp.cpp @@ -20,6 +20,7 @@ LUAU_FASTFLAGVARIABLE(DebugLuauAbortingChecks, false) LUAU_FASTFLAG(LuauCodegenVectorTag2) LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauCodeGenCoverForgprepEffect, false) LUAU_FASTFLAG(LuauCodegenLoadTVTag) +LUAU_FASTFLAG(LuauCodegenRemoveDeadStores3) namespace Luau { @@ -1072,8 +1073,39 @@ static void constPropInInst(ConstPropState& state, IrBuilder& build, IrFunction& } break; - // TODO: FASTCALL is more restrictive than INVOKE_FASTCALL; we should either determine the exact semantics, or rework it case IrCmd::FASTCALL: + { + if (FFlag::LuauCodegenRemoveDeadStores3) + { + LuauBuiltinFunction bfid = LuauBuiltinFunction(function.uintOp(inst.a)); + int firstReturnReg = vmRegOp(inst.b); + int nresults = function.intOp(inst.f); + + // TODO: FASTCALL is more restrictive than INVOKE_FASTCALL; we should either determine the exact semantics, or rework it + handleBuiltinEffects(state, bfid, firstReturnReg, nresults); + + switch (bfid) + { + case LBF_MATH_MODF: + case LBF_MATH_FREXP: + state.updateTag(IrOp{IrOpKind::VmReg, uint8_t(firstReturnReg)}, LUA_TNUMBER); + + if (nresults > 1) + state.updateTag(IrOp{IrOpKind::VmReg, uint8_t(firstReturnReg + 1)}, LUA_TNUMBER); + break; + case LBF_MATH_SIGN: + state.updateTag(IrOp{IrOpKind::VmReg, uint8_t(firstReturnReg)}, LUA_TNUMBER); + break; + default: + break; + } + } + else + { + handleBuiltinEffects(state, LuauBuiltinFunction(function.uintOp(inst.a)), vmRegOp(inst.b), function.intOp(inst.f)); + } + break; + } case IrCmd::INVOKE_FASTCALL: handleBuiltinEffects(state, LuauBuiltinFunction(function.uintOp(inst.a)), vmRegOp(inst.b), function.intOp(inst.f)); break; diff --git a/CodeGen/src/OptimizeDeadStore.cpp b/CodeGen/src/OptimizeDeadStore.cpp index 3ea066e44..25b7fed4b 100644 --- a/CodeGen/src/OptimizeDeadStore.cpp +++ b/CodeGen/src/OptimizeDeadStore.cpp @@ -9,7 +9,7 @@ #include "lobject.h" -LUAU_FASTFLAGVARIABLE(LuauCodegenRemoveDeadStores2, false) +LUAU_FASTFLAGVARIABLE(LuauCodegenRemoveDeadStores3, false) LUAU_FASTFLAG(LuauCodegenVectorTag2) // TODO: optimization can be improved by knowing which registers are live in at each VM exit @@ -265,6 +265,12 @@ static void markDeadStoresInInst(RemoveDeadStoreState& state, IrBuilder& build, uint8_t tag = function.tagOp(inst.b); + // Storing 'nil' TValue doesn't update the value part because we don't care about that part of 'nil' + // This however prevents us from removing unused value store elimination and has an impact on GC + // To solve this issues, we invalidate the value part of a 'nil' store as well + if (tag == LUA_TNIL) + state.killValueStore(regInfo); + regInfo.tagInstIdx = index; regInfo.maybeGco = isGCO(tag); state.hasGcoToClear |= regInfo.maybeGco; diff --git a/Sources.cmake b/Sources.cmake index 294ce7aa5..5db4b83bd 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -259,6 +259,7 @@ target_sources(Luau.Analysis PRIVATE Analysis/src/Refinement.cpp Analysis/src/RequireTracer.cpp Analysis/src/Scope.cpp + Analysis/src/Set.cpp Analysis/src/Simplify.cpp Analysis/src/Substitution.cpp Analysis/src/Subtyping.cpp diff --git a/VM/src/ldblib.cpp b/VM/src/ldblib.cpp index cf564e98e..77c1befcc 100644 --- a/VM/src/ldblib.cpp +++ b/VM/src/ldblib.cpp @@ -8,6 +8,8 @@ #include #include +LUAU_DYNAMIC_FASTFLAGVARIABLE(LuauDebugInfoDupArgLeftovers, false) + static lua_State* getthread(lua_State* L, int* arg) { if (lua_isthread(L, 1)) @@ -26,10 +28,17 @@ static int db_info(lua_State* L) { int arg; lua_State* L1 = getthread(L, &arg); + int l1top = 0; - // If L1 != L, L1 can be in any state, and therefore there are no guarantees about its stack space + // if L1 != L, L1 can be in any state, and therefore there are no guarantees about its stack space if (L != L1) - lua_rawcheckstack(L1, 1); // for 'f' option + { + // for 'f' option, we reserve one slot and we also record the stack top + lua_rawcheckstack(L1, 1); + + if (DFFlag::LuauDebugInfoDupArgLeftovers) + l1top = lua_gettop(L1); + } int level; if (lua_isnumber(L, arg + 1)) @@ -59,7 +68,13 @@ static int db_info(lua_State* L) if (unsigned(*it - 'a') < 26) { if (occurs[*it - 'a']) + { + // restore stack state of another thread as 'f' option might not have been visited yet + if (DFFlag::LuauDebugInfoDupArgLeftovers && L != L1) + lua_settop(L1, l1top); + luaL_argerror(L, arg + 2, "duplicate option"); + } occurs[*it - 'a'] = true; } diff --git a/VM/src/ldo.cpp b/VM/src/ldo.cpp index d13e98f37..453fd9670 100644 --- a/VM/src/ldo.cpp +++ b/VM/src/ldo.cpp @@ -104,6 +104,11 @@ class lua_exception : public std::exception return status; } + const lua_State* getThread() const + { + return L; + } + private: lua_State* L; int status; @@ -120,7 +125,12 @@ int luaD_rawrunprotected(lua_State* L, Pfunc f, void* ud) } catch (lua_exception& e) { - // lua_exception means that luaD_throw was called and an exception object is on stack if status is ERRRUN + // It is assumed/required that the exception caught here was thrown from the same Luau state. + // If this assert fires, it indicates a lua_exception was not properly caught and propagated + // to the exception handler for a different Luau state. Report this issue to the Luau team if + // you need more information or assistance resolving this assert. + LUAU_ASSERT(e.getThread() == L); + status = e.getStatus(); } catch (std::exception& e) diff --git a/VM/src/lfunc.cpp b/VM/src/lfunc.cpp index 276598fe5..362d01251 100644 --- a/VM/src/lfunc.cpp +++ b/VM/src/lfunc.cpp @@ -6,37 +6,85 @@ #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); - 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; + + 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; + } return f; } diff --git a/VM/src/lgcdebug.cpp b/VM/src/lgcdebug.cpp index 48a6d2e2f..bac7ccb8d 100644 --- a/VM/src/lgcdebug.cpp +++ b/VM/src/lgcdebug.cpp @@ -14,6 +14,8 @@ #include #include +LUAU_FASTFLAGVARIABLE(LuauCodegenHeapSizeReport, false) + static void validateobjref(global_State* g, GCObject* f, GCObject* t) { LUAU_ASSERT(!isdead(g, t)); @@ -824,6 +826,17 @@ static void enumproto(EnumContext* ctx, Proto* p) size_t size = sizeof(Proto) + sizeof(Instruction) * p->sizecode + sizeof(Proto*) * p->sizep + sizeof(TValue) * p->sizek + p->sizelineinfo + sizeof(LocVar) * p->sizelocvars + sizeof(TString*) * p->sizeupvalues; + if (FFlag::LuauCodegenHeapSizeReport) + { + if (p->execdata && ctx->L->global->ecb.getmemorysize) + { + size_t nativesize = ctx->L->global->ecb.getmemorysize(ctx->L, p); + + ctx->node(ctx->context, p->execdata, uint8_t(LUA_TNONE), p->memcat, nativesize, NULL); + ctx->edge(ctx->context, enumtopointer(obj2gco(p)), p->execdata, "[native]"); + } + } + enumnode(ctx, obj2gco(p), size, p->source ? getstr(p->source) : NULL); if (p->sizek) diff --git a/VM/src/lstate.h b/VM/src/lstate.h index a33c882f0..97546511d 100644 --- a/VM/src/lstate.h +++ b/VM/src/lstate.h @@ -155,6 +155,7 @@ struct lua_ExecutionCallbacks void (*destroy)(lua_State* L, Proto* proto); // called when function is destroyed int (*enter)(lua_State* L, Proto* proto); // called when function is about to start/resume (when execdata is present), return 0 to exit VM void (*disable)(lua_State* L, Proto* proto); // called when function has to be switched from native to bytecode in the debugger + size_t (*getmemorysize)(lua_State* L, Proto* proto); // called to request the size of memory associated with native part of the Proto }; /* diff --git a/VM/src/lvmload.cpp b/VM/src/lvmload.cpp index 98d74452b..2f73d8bc0 100644 --- a/VM/src/lvmload.cpp +++ b/VM/src/lvmload.cpp @@ -13,6 +13,8 @@ #include +LUAU_FASTFLAGVARIABLE(LuauLoadExceptionSafe, false) + // TODO: RAII deallocation doesn't work for longjmp builds if a memory error happens template struct TempBuffer @@ -28,7 +30,13 @@ struct TempBuffer { } - ~TempBuffer() + TempBuffer(const TempBuffer&) = delete; + TempBuffer(TempBuffer&&) = delete; + + TempBuffer& operator=(const TempBuffer&) = delete; + TempBuffer& operator=(TempBuffer&&) = delete; + + ~TempBuffer() noexcept { luaM_freearray(L, data, count, T, 0); } @@ -40,6 +48,38 @@ struct TempBuffer } }; +struct ScopedSetGCThreshold +{ +public: + ScopedSetGCThreshold(global_State* global, size_t newThreshold) noexcept + : global{global} + { + if (FFlag::LuauLoadExceptionSafe) + { + originalThreshold = global->GCthreshold; + global->GCthreshold = newThreshold; + } + } + + ScopedSetGCThreshold(const ScopedSetGCThreshold&) = delete; + ScopedSetGCThreshold(ScopedSetGCThreshold&&) = delete; + + ScopedSetGCThreshold& operator=(const ScopedSetGCThreshold&) = delete; + ScopedSetGCThreshold& operator=(ScopedSetGCThreshold&&) = delete; + + ~ScopedSetGCThreshold() noexcept + { + if (FFlag::LuauLoadExceptionSafe) + { + global->GCthreshold = originalThreshold; + } + } + +private: + global_State* global = nullptr; + size_t originalThreshold = 0; +}; + void luaV_getimport(lua_State* L, Table* env, TValue* k, StkId res, uint32_t id, bool propagatenil) { int count = id >> 30; @@ -181,9 +221,13 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size luaC_checkGC(L); // pause GC for the duration of deserialization - some objects we're creating aren't rooted - // TODO: if an allocation error happens mid-load, we do not unpause GC! + const ScopedSetGCThreshold pauseGC{L->global, SIZE_MAX}; size_t GCthreshold = L->global->GCthreshold; - L->global->GCthreshold = SIZE_MAX; + + 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)); @@ -245,31 +289,55 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size offset += typesize; } - p->sizecode = readVarInt(data, size, offset); - p->code = luaM_newarray(L, p->sizecode, Instruction, p->memcat); + 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); + } + for (int j = 0; j < p->sizecode; ++j) p->code[j] = read(data, size, offset); p->codeentry = p->code; - p->sizek = readVarInt(data, size, offset); - p->k = luaM_newarray(L, p->sizek, TValue, p->memcat); + 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); + } -#ifdef HARDMEMTESTS - // this is redundant during normal runs, but resolveImportSafe can trigger GC checks under HARDMEMTESTS - // because p->k isn't fully formed at this point, we pre-fill it with nil to make subsequent setup safe - for (int j = 0; j < p->sizek; ++j) + if (FFlag::LuauLoadExceptionSafe) { - setnilvalue(&p->k[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]); + } } -#endif for (int j = 0; j < p->sizek; ++j) { switch (read(data, size, offset)) { case LBC_CONSTANT_NIL: - setnilvalue(&p->k[j]); + if (!FFlag::LuauLoadExceptionSafe) + { + setnilvalue(&p->k[j]); + } break; case LBC_CONSTANT_BOOLEAN: @@ -341,8 +409,18 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size } } - p->sizep = readVarInt(data, size, offset); - p->p = luaM_newarray(L, p->sizep, Proto*, p->memcat); + 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); + } + for (int j = 0; j < p->sizep; ++j) { uint32_t fid = readVarInt(data, size, offset); @@ -361,8 +439,18 @@ 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; - p->sizelineinfo = absoffset + intervals * sizeof(int); - p->lineinfo = luaM_newarray(L, p->sizelineinfo, uint8_t, p->memcat); + 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); + } + p->abslineinfo = (int*)(p->lineinfo + absoffset); uint8_t lastoffset = 0; @@ -384,8 +472,17 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size if (debuginfo) { - p->sizelocvars = readVarInt(data, size, offset); - p->locvars = luaM_newarray(L, p->sizelocvars, LocVar, p->memcat); + 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); + } for (int j = 0; j < p->sizelocvars; ++j) { @@ -395,8 +492,17 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size p->locvars[j].reg = read(data, size, offset); } - p->sizeupvalues = readVarInt(data, size, offset); - p->upvalues = luaM_newarray(L, p->sizeupvalues, TString*, p->memcat); + 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); + } for (int j = 0; j < p->sizeupvalues; ++j) { @@ -417,7 +523,10 @@ int luau_load(lua_State* L, const char* chunkname, const char* data, size_t size setclvalue(L, L->top, cl); incr_top(L); - L->global->GCthreshold = GCthreshold; + if (!FFlag::LuauLoadExceptionSafe) + { + L->global->GCthreshold = GCthreshold; + } return 0; } diff --git a/tests/AssemblyBuilderA64.test.cpp b/tests/AssemblyBuilderA64.test.cpp index 3942003b4..60de435d5 100644 --- a/tests/AssemblyBuilderA64.test.cpp +++ b/tests/AssemblyBuilderA64.test.cpp @@ -1,11 +1,14 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/AssemblyBuilderA64.h" #include "Luau/StringUtils.h" +#include "ScopedFlags.h" #include "doctest.h" #include +LUAU_FASTFLAG(LuauCodeGenOptVecA64) + using namespace Luau::CodeGen; using namespace Luau::CodeGen::A64; @@ -448,6 +451,8 @@ TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "FPCompare") TEST_CASE_FIXTURE(AssemblyBuilderA64Fixture, "FPImm") { + ScopedFastFlag luauCodeGenOptVecA64{FFlag::LuauCodeGenOptVecA64, true}; + SINGLE_COMPARE(fmov(d0, 0), 0x2F00E400); SINGLE_COMPARE(fmov(d0, 0.125), 0x1E681000); SINGLE_COMPARE(fmov(d0, -0.125), 0x1E781000); diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 560f011f4..6d4d3e47f 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -16,7 +16,6 @@ LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2) LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) LUAU_FASTFLAG(LuauAutocompleteStringLiteralBounds); -LUAU_FASTFLAG(LuauAutocompleteTableKeysNoInitialCharacter) using namespace Luau; @@ -2696,8 +2695,6 @@ local t = { TEST_CASE_FIXTURE(ACFixture, "suggest_table_keys_no_initial_character") { - ScopedFastFlag sff{FFlag::LuauAutocompleteTableKeysNoInitialCharacter, true}; - check(R"( type Test = { first: number, second: number } local t: Test = { @1 } @@ -2711,8 +2708,6 @@ local t: Test = { @1 } TEST_CASE_FIXTURE(ACFixture, "suggest_table_keys_no_initial_character_2") { - ScopedFastFlag sff{FFlag::LuauAutocompleteTableKeysNoInitialCharacter, true}; - check(R"( type Test = { first: number, second: number } local t: Test = { first = 1, @1 } @@ -2726,8 +2721,6 @@ local t: Test = { first = 1, @1 } TEST_CASE_FIXTURE(ACFixture, "suggest_table_keys_no_initial_character_3") { - ScopedFastFlag sff{FFlag::LuauAutocompleteTableKeysNoInitialCharacter, true}; - check(R"( type Properties = { TextScaled: boolean, Text: string } local function create(props: Properties) end @@ -3642,6 +3635,9 @@ TEST_CASE_FIXTURE(ACFixture, "string_contents_is_available_to_callback") TEST_CASE_FIXTURE(ACFixture, "autocomplete_response_perf1" * doctest::timeout(0.5)) { + if (FFlag::DebugLuauDeferredConstraintResolution) + return; // FIXME: This test is just barely at the threshhold which makes it very flaky under the new solver + // Build a function type with a large overload set const int parts = 100; std::string source; diff --git a/tests/Conformance.test.cpp b/tests/Conformance.test.cpp index 8f217783f..baa5639bf 100644 --- a/tests/Conformance.test.cpp +++ b/tests/Conformance.test.cpp @@ -26,8 +26,13 @@ extern bool verbose; extern bool codegen; extern int optimizationLevel; +// internal functions, declared in lgc.h - not exposed via lua.h +void luaC_fullgc(lua_State* L); +void luaC_validate(lua_State* L); + LUAU_FASTINT(CodegenHeuristicsInstructionLimit) -LUAU_DYNAMIC_FASTFLAG(LuauCodegenTrackingMultilocationFix) +LUAU_FASTFLAG(LuauLoadExceptionSafe) +LUAU_DYNAMIC_FASTFLAG(LuauDebugInfoDupArgLeftovers) static lua_CompileOptions defaultOptions() { @@ -241,7 +246,6 @@ static StateRef runConformance(const char* name, void (*setup)(lua_State* L) = n status = lua_resume(L, nullptr, 0); } - extern void luaC_validate(lua_State * L); // internal function, declared in lgc.h - not exposed via lua.h luaC_validate(L); if (status == 0) @@ -626,6 +630,8 @@ TEST_CASE("DateTime") TEST_CASE("Debug") { + ScopedFastFlag luauDebugInfoDupArgLeftovers{DFFlag::LuauDebugInfoDupArgLeftovers, true}; + runConformance("debug.lua"); } @@ -2030,8 +2036,6 @@ TEST_CASE("SafeEnv") TEST_CASE("Native") { - ScopedFastFlag luauCodegenTrackingMultilocationFix{DFFlag::LuauCodegenTrackingMultilocationFix, true}; - // This tests requires code to run natively, otherwise all 'is_native' checks will fail if (!codegen || !luau_codegen_supported()) return; @@ -2053,7 +2057,7 @@ TEST_CASE("NativeTypeAnnotations") }); } -TEST_CASE("HugeFunction") +[[nodiscard]] static std::string makeHugeFunctionSource() { std::string source; @@ -2074,6 +2078,13 @@ TEST_CASE("HugeFunction") // use failed fast-calls with imports and constants to exercise all of the more complex fallback sequences source += "return bit32.lshift('84', -1)"; + return source; +} + +TEST_CASE("HugeFunction") +{ + std::string source = makeHugeFunctionSource(); + StateRef globalState(luaL_newstate(), lua_close); lua_State* L = globalState.get(); @@ -2100,6 +2111,78 @@ TEST_CASE("HugeFunction") CHECK(lua_tonumber(L, -1) == 42); } +TEST_CASE("HugeFunctionLoadFailure") +{ + // This test case verifies that if an out-of-memory error occurs inside of + // luau_load, we are not left with any GC objects in inconsistent states + // that would cause issues during garbage collection. + // + // We create a script with a huge function in it, then pass this to + // 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; + + static size_t largeAllocationToFail = 0; + static size_t largeAllocationCount = 0; + + const auto testAllocate = [](void* ud, void* ptr, size_t osize, size_t nsize) -> void* + { + if (nsize == 0) + { + free(ptr); + return nullptr; + } + else if (nsize > 32768) + { + if (largeAllocationCount == largeAllocationToFail) + return nullptr; + + ++largeAllocationCount; + return realloc(ptr, nsize); + } + else + { + return realloc(ptr, nsize); + } + }; + + size_t bytecodeSize = 0; + char* const bytecode = luau_compile(source.data(), source.size(), nullptr, &bytecodeSize); + + for (largeAllocationToFail = 0; largeAllocationToFail != expectedTotalLargeAllocations; ++largeAllocationToFail) + { + largeAllocationCount = 0; + + StateRef globalState(lua_newstate(testAllocate, nullptr), lua_close); + lua_State* L = globalState.get(); + + luaL_openlibs(L); + luaL_sandbox(L); + luaL_sandboxthread(L); + + try + { + luau_load(L, "=HugeFunction", bytecode, bytecodeSize, 0); + REQUIRE(false); // The luau_load should fail with an exception + } + catch (const std::exception& ex) + { + REQUIRE(strcmp(ex.what(), "lua_exception: not enough memory") == 0); + } + + luaC_fullgc(L); + } + + free(bytecode); + + REQUIRE_EQ(largeAllocationToFail, expectedTotalLargeAllocations); +} + TEST_CASE("IrInstructionLimit") { if (!codegen || !luau_codegen_supported()) diff --git a/tests/IrBuilder.test.cpp b/tests/IrBuilder.test.cpp index 83d84f63b..00ab9e982 100644 --- a/tests/IrBuilder.test.cpp +++ b/tests/IrBuilder.test.cpp @@ -13,13 +13,11 @@ #include LUAU_FASTFLAG(LuauCodegenVectorTag2) +LUAU_FASTFLAG(LuauCodegenRemoveDeadStores3) +LUAU_FASTFLAG(DebugLuauAbortingChecks) using namespace Luau::CodeGen; -LUAU_FASTFLAG(LuauCodegenRemoveDeadStores2) - -LUAU_FASTFLAG(DebugLuauAbortingChecks) - class IrBuilderFixture { public: @@ -2543,7 +2541,7 @@ bb_0: ; useCount: 0 TEST_CASE_FIXTURE(IrBuilderFixture, "ForgprepInvalidation") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; IrOp block = build.block(IrBlockKind::Internal); IrOp followup = build.block(IrBlockKind::Internal); @@ -2582,6 +2580,57 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "ForgprepInvalidation") )"); } +TEST_CASE_FIXTURE(IrBuilderFixture, "FastCallEffects1") +{ + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; + + IrOp entry = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + build.inst(IrCmd::FASTCALL, build.constUint(LBF_MATH_FREXP), build.vmReg(1), build.vmReg(2), build.undef(), build.constInt(1), build.constInt(2)); + build.inst(IrCmd::CHECK_TAG, build.vmReg(1), build.constTag(tnumber), build.vmExit(1)); + build.inst(IrCmd::CHECK_TAG, build.vmReg(2), build.constTag(tnumber), build.vmExit(1)); + build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(2)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + constPropInBlockChains(build, true); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: +; in regs: R2 + FASTCALL 14u, R1, R2, undef, 1i, 2i + RETURN R1, 2i + +)"); +} + +TEST_CASE_FIXTURE(IrBuilderFixture, "FastCallEffects2") +{ + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; + + IrOp entry = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + build.inst(IrCmd::FASTCALL, build.constUint(LBF_MATH_MODF), build.vmReg(1), build.vmReg(2), build.undef(), build.constInt(1), build.constInt(1)); + build.inst(IrCmd::CHECK_TAG, build.vmReg(1), build.constTag(tnumber), build.vmExit(1)); + build.inst(IrCmd::CHECK_TAG, build.vmReg(2), build.constTag(tnumber), build.vmExit(1)); + build.inst(IrCmd::RETURN, build.vmReg(1), build.constInt(2)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + constPropInBlockChains(build, true); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: +; in regs: R2 + FASTCALL 20u, R1, R2, undef, 1i, 1i + CHECK_TAG R2, tnumber, exit(1) + RETURN R1, 2i + +)"); +} + TEST_SUITE_END(); TEST_SUITE_BEGIN("Analysis"); @@ -2893,7 +2942,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "BuiltinVariadicStart") TEST_CASE_FIXTURE(IrBuilderFixture, "ForgprepImplicitUse") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; IrOp entry = build.block(IrBlockKind::Internal); IrOp direct = build.block(IrBlockKind::Internal); @@ -3403,7 +3452,7 @@ TEST_SUITE_BEGIN("DeadStoreRemoval"); TEST_CASE_FIXTURE(IrBuilderFixture, "SimpleDoubleStore") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; IrOp entry = build.block(IrBlockKind::Internal); @@ -3449,7 +3498,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "SimpleDoubleStore") TEST_CASE_FIXTURE(IrBuilderFixture, "UnusedAtReturn") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; IrOp entry = build.block(IrBlockKind::Internal); @@ -3479,7 +3528,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "UnusedAtReturn") TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse1") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; IrOp entry = build.block(IrBlockKind::Internal); @@ -3509,7 +3558,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse1") TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse2") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; IrOp entry = build.block(IrBlockKind::Internal); @@ -3543,7 +3592,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse2") TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse3") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; IrOp entry = build.block(IrBlockKind::Internal); @@ -3571,9 +3620,68 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse3") )"); } +TEST_CASE_FIXTURE(IrBuilderFixture, "HiddenPointerUse4") +{ + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; + + IrOp entry = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(0), build.constDouble(1.0)); + build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnumber)); + build.inst(IrCmd::CHECK_GC); + build.inst(IrCmd::STORE_TAG, build.vmReg(0), build.constTag(tnil)); + build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + constPropInBlockChains(build, true); + markDeadStoresInBlockChains(build); + + // It is important for tag overwrite to TNIL to kill not only the previous tag store, but the value as well + // This is important in a following scenario: + // - R0 might have been a GCO on entry to bb_0 + // - R0 is overwritten by a number + // - Stack is visited by GC assist + // - R0 is overwritten by nil + // If only number tag write would have been killed, there will be a GCO tag with a double value on stack + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + CHECK_GC + STORE_TAG R0, tnil + RETURN R0, 1i + +)"); +} + +TEST_CASE_FIXTURE(IrBuilderFixture, "PartialVsFullStoresWithRecombination") +{ + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; + + IrOp entry = build.block(IrBlockKind::Internal); + + build.beginBlock(entry); + build.inst(IrCmd::STORE_DOUBLE, build.vmReg(1), build.constDouble(1.0)); + build.inst(IrCmd::STORE_TAG, build.vmReg(1), build.constTag(tnumber)); + build.inst(IrCmd::STORE_TVALUE, build.vmReg(0), build.inst(IrCmd::LOAD_TVALUE, build.vmReg(1))); + build.inst(IrCmd::RETURN, build.vmReg(0), build.constInt(1)); + + updateUseCounts(build.function); + computeCfgInfo(build.function); + constPropInBlockChains(build, true); + markDeadStoresInBlockChains(build); + + CHECK("\n" + toString(build.function, IncludeUseInfo::No) == R"( +bb_0: + STORE_SPLIT_TVALUE R0, tnumber, 1 + RETURN R0, 1i + +)"); +} + TEST_CASE_FIXTURE(IrBuilderFixture, "IgnoreFastcallAdjustment") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; IrOp entry = build.block(IrBlockKind::Internal); @@ -3602,7 +3710,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "IgnoreFastcallAdjustment") TEST_CASE_FIXTURE(IrBuilderFixture, "JumpImplicitLiveOut") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; IrOp entry = build.block(IrBlockKind::Internal); IrOp next = build.block(IrBlockKind::Internal); @@ -3639,7 +3747,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "JumpImplicitLiveOut") TEST_CASE_FIXTURE(IrBuilderFixture, "KeepCapturedRegisterStores") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; IrOp entry = build.block(IrBlockKind::Internal); @@ -3678,7 +3786,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "KeepCapturedRegisterStores") TEST_CASE_FIXTURE(IrBuilderFixture, "AbortingChecksRequireStores") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; ScopedFastFlag debugLuauAbortingChecks{FFlag::DebugLuauAbortingChecks, true}; IrOp block = build.block(IrBlockKind::Internal); @@ -3719,7 +3827,7 @@ TEST_CASE_FIXTURE(IrBuilderFixture, "AbortingChecksRequireStores") TEST_CASE_FIXTURE(IrBuilderFixture, "PartialOverFullValue") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; IrOp entry = build.block(IrBlockKind::Internal); diff --git a/tests/IrLowering.test.cpp b/tests/IrLowering.test.cpp index e81406373..12a873090 100644 --- a/tests/IrLowering.test.cpp +++ b/tests/IrLowering.test.cpp @@ -13,7 +13,7 @@ #include LUAU_FASTFLAG(LuauCodegenVectorTag2) -LUAU_FASTFLAG(LuauCodegenRemoveDeadStores2) +LUAU_FASTFLAG(LuauCodegenRemoveDeadStores3) static std::string getCodegenAssembly(const char* source) { @@ -91,7 +91,7 @@ end TEST_CASE("VectorComponentRead") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; CHECK_EQ("\n" + getCodegenAssembly(R"( local function compsum(a: vector) @@ -175,7 +175,7 @@ end TEST_CASE("VectorSubMulDiv") { ScopedFastFlag luauCodegenVectorTag2{FFlag::LuauCodegenVectorTag2, true}; - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; CHECK_EQ("\n" + getCodegenAssembly(R"( local function vec3combo(a: vector, b: vector, c: vector, d: vector) @@ -210,7 +210,7 @@ end TEST_CASE("VectorSubMulDiv2") { ScopedFastFlag luauCodegenVectorTag2{FFlag::LuauCodegenVectorTag2, true}; - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; CHECK_EQ("\n" + getCodegenAssembly(R"( local function vec3combo(a: vector) @@ -241,7 +241,7 @@ end TEST_CASE("VectorMulDivMixed") { ScopedFastFlag luauCodegenVectorTag2{FFlag::LuauCodegenVectorTag2, true}; - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; CHECK_EQ("\n" + getCodegenAssembly(R"( local function vec3combo(a: vector, b: vector, c: vector, d: vector) @@ -283,7 +283,7 @@ end TEST_CASE("ExtraMathMemoryOperands") { - ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores2, true}; + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; CHECK_EQ("\n" + getCodegenAssembly(R"( local function foo(a: number, b: number, c: number, d: number, e: number) @@ -319,4 +319,86 @@ end )"); } +TEST_CASE("DseInitialStackState") +{ + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; + + CHECK_EQ("\n" + getCodegenAssembly(R"( +local function foo() + while {} do + local _ = not _,{} + _ = nil + end +end +)"), + R"( +; function foo() line 2 +bb_bytecode_0: + SET_SAVEDPC 1u + %1 = NEW_TABLE 0u, 0u + STORE_POINTER R0, %1 + STORE_TAG R0, ttable + CHECK_GC + JUMP bb_2 +bb_2: + CHECK_SAFE_ENV exit(3) + JUMP_EQ_TAG K1, tnil, bb_fallback_4, bb_3 +bb_3: + %9 = LOAD_TVALUE K1 + STORE_TVALUE R1, %9 + JUMP bb_5 +bb_5: + SET_SAVEDPC 7u + %21 = NEW_TABLE 0u, 0u + STORE_POINTER R1, %21 + STORE_TAG R1, ttable + CHECK_GC + STORE_TAG R0, tnil + INTERRUPT 9u + JUMP bb_bytecode_0 +)"); +} + +TEST_CASE("DseInitialStackState2") +{ + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; + + CHECK_EQ("\n" + getCodegenAssembly(R"( +local function foo(a) + math.frexp(a) + return a +end +)"), + R"( +; function foo($arg0) line 2 +bb_bytecode_0: + CHECK_SAFE_ENV exit(1) + CHECK_TAG R0, tnumber, exit(1) + FASTCALL 14u, R1, R0, undef, 1i, 2i + INTERRUPT 5u + RETURN R0, 1i +)"); +} + +TEST_CASE("DseInitialStackState3") +{ + ScopedFastFlag luauCodegenRemoveDeadStores{FFlag::LuauCodegenRemoveDeadStores3, true}; + + CHECK_EQ("\n" + getCodegenAssembly(R"( +local function foo(a) + math.sign(a) + return a +end +)"), + R"( +; function foo($arg0) line 2 +bb_bytecode_0: + CHECK_SAFE_ENV exit(1) + CHECK_TAG R0, tnumber, exit(1) + FASTCALL 47u, R1, R0, undef, 1i, 1i + INTERRUPT 5u + RETURN R0, 1i +)"); +} + TEST_SUITE_END(); diff --git a/tests/RequireByString.test.cpp b/tests/RequireByString.test.cpp index 94d7fd45b..39ed571ef 100644 --- a/tests/RequireByString.test.cpp +++ b/tests/RequireByString.test.cpp @@ -57,7 +57,8 @@ class ReplWithPathFixture for (int i = 0; i < 20; ++i) { bool engineTestDir = isDirectory(luauDirAbs + "/Client/Luau/tests"); - bool luauTestDir = isDirectory(luauDirAbs + "/luau/tests"); + bool luauTestDir = isDirectory(luauDirAbs + "/luau/tests/require"); + if (engineTestDir || luauTestDir) { if (engineTestDir) diff --git a/tests/Set.test.cpp b/tests/Set.test.cpp index a70f4f61b..a3a6d05ea 100644 --- a/tests/Set.test.cpp +++ b/tests/Set.test.cpp @@ -1,8 +1,14 @@ // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details +#include "ScopedFlags.h" #include "Luau/Set.h" #include "doctest.h" +#include +#include + +LUAU_FASTFLAG(LuauFixSetIter); + TEST_SUITE_BEGIN("SetTests"); TEST_CASE("empty_set_size_0") @@ -99,4 +105,32 @@ TEST_CASE("iterate_over_set_skips_erased_elements") CHECK(sum == 9); } +TEST_CASE("iterate_over_set_skips_first_element_if_it_is_erased") +{ + ScopedFastFlag sff{FFlag::LuauFixSetIter, true}; + + /* + * As of this writing, in the following set, the key "y" happens to occur + * before "x" in the underlying DenseHashSet. This is important because it + * surfaces something that Set::const_iterator needs to do: If the + * underlying iterator happens to start at a deleted element, we need to + * advance until we find the first live element (or the end of the set). + */ + Luau::Set s1{{}}; + s1.insert("x"); + s1.insert("y"); + s1.erase("y"); + + std::vector out; + auto it = s1.begin(); + auto endIt = s1.end(); + while (it != endIt) + { + out.push_back(*it); + ++it; + } + + CHECK(1 == out.size()); +} + TEST_SUITE_END(); diff --git a/tests/Simplify.test.cpp b/tests/Simplify.test.cpp index 66f7dcd35..83f1d87d0 100644 --- a/tests/Simplify.test.cpp +++ b/tests/Simplify.test.cpp @@ -9,6 +9,7 @@ using namespace Luau; LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) +LUAU_DYNAMIC_FASTINT(LuauSimplificationComplexityLimit) namespace { @@ -133,8 +134,8 @@ TEST_CASE_FIXTURE(SimplifyFixture, "unknown_and_other_tops_and_bottom_types") CHECK(unknownTy == intersect(unknownTy, unknownTy)); - CHECK("*error-type* | unknown" == intersectStr(unknownTy, anyTy)); - CHECK("*error-type* | unknown" == intersectStr(anyTy, unknownTy)); + CHECK("any" == intersectStr(unknownTy, anyTy)); + CHECK("any" == intersectStr(anyTy, unknownTy)); CHECK(neverTy == intersect(unknownTy, neverTy)); CHECK(neverTy == intersect(neverTy, unknownTy)); @@ -443,6 +444,7 @@ TEST_CASE_FIXTURE(SimplifyFixture, "union") TEST_CASE_FIXTURE(SimplifyFixture, "two_unions") { + ScopedFastInt sfi{DFInt::LuauSimplificationComplexityLimit, 10}; TypeId t1 = arena->addType(UnionType{{numberTy, booleanTy, stringTy, nilTy, tableTy}}); CHECK("false?" == intersectStr(t1, falsyTy)); diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index 0fdea695b..000f0eccd 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -693,4 +693,18 @@ TEST_CASE_FIXTURE(Fixture, "read_write_class_properties") CHECK(builtinTypes->numberType == tm->givenType); } +TEST_CASE_FIXTURE(ClassFixture, "cannot_index_a_class_with_no_indexer") +{ + CheckResult result = check(R"( + local a = BaseClass.New() + + local c = a[1] + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK_MESSAGE(get(result.errors[0]), "Expected DynamicPropertyLookupOnClassesUnsafe but got " << result.errors[0]); + + CHECK(builtinTypes->errorType == requireType("c")); +} TEST_SUITE_END(); diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index 0b316150e..da23e3f2a 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -1141,4 +1141,42 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "luau_roact_useState_minimization") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(BuiltinsFixture, "bin_prov") +{ + CheckResult result = check(R"( + local Bin = {} + + function Bin:add(item) + self.head = { item = item} + return item + end + + function Bin:destroy() + while self.head do + local item = self.head.item + if type(item) == "function" then + item() + elseif item.Destroy ~= nil then + end + self.head = self.head.next + end + end + )"); +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "update_phonemes_minimized") +{ + CheckResult result = check(R"( + local video + function(response) + for index = 1, #response do + video = video + end + return video + end + )"); + + LUAU_REQUIRE_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index 9a356c59e..7408c799a 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -323,6 +323,99 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "typeguard_in_assert_position") REQUIRE_EQ("number", toString(requireType("b"))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table_then_test_a_prop") +{ + CheckResult result = check(R"( + local function f(x: unknown): string? + if typeof(x) == "table" then + if typeof(x.foo) == "string" then + return x.foo + end + end + + return nil + end + )"); + + if (FFlag::DebugLuauDeferredConstraintResolution) + LUAU_REQUIRE_NO_ERRORS(result); + else + { + LUAU_REQUIRE_ERROR_COUNT(2, result); + + for (size_t i = 0; i < result.errors.size(); i++) + { + const UnknownProperty* up = get(result.errors[i]); + REQUIRE_EQ("foo", up->key); + REQUIRE_EQ("unknown", toString(up->table)); + } + } +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table_then_test_a_nested_prop") +{ + CheckResult result = check(R"( + local function f(x: unknown): string? + if typeof(x) == "table" then + -- this should error, `x.foo` is an unknown property + if typeof(x.foo.bar) == "string" then + return x.foo.bar + end + end + + return nil + end + )"); + + if (FFlag::DebugLuauDeferredConstraintResolution) + { + LUAU_REQUIRE_ERROR_COUNT(1, result); + const UnknownProperty* up = get(result.errors[0]); + REQUIRE_EQ("bar", up->key); + REQUIRE_EQ("unknown", toString(up->table)); + } + else + { + LUAU_REQUIRE_ERROR_COUNT(2, result); + + for (size_t i = 0; i < result.errors.size(); i++) + { + const UnknownProperty* up = get(result.errors[i]); + REQUIRE_EQ("foo", up->key); + REQUIRE_EQ("unknown", toString(up->table)); + } + } +} + +TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table_then_test_a_tested_nested_prop") +{ + CheckResult result = check(R"( + local function f(x: unknown): string? + if typeof(x) == "table" then + if typeof(x.foo) == "table" and typeof(x.foo.bar) == "string" then + return x.foo.bar + end + end + + return nil + end + )"); + + if (FFlag::DebugLuauDeferredConstraintResolution) + LUAU_REQUIRE_NO_ERRORS(result); + else + { + LUAU_REQUIRE_ERROR_COUNT(3, result); + + for (size_t i = 0; i < result.errors.size(); i++) + { + const UnknownProperty* up = get(result.errors[i]); + REQUIRE_EQ("foo", up->key); + REQUIRE_EQ("unknown", toString(up->table)); + } + } +} + TEST_CASE_FIXTURE(BuiltinsFixture, "call_an_incompatible_function_after_using_typeguard") { CheckResult result = check(R"( @@ -2057,4 +2150,5 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "mutate_prop_of_some_refined_symbol_2" LUAU_REQUIRE_NO_ERRORS(result); } + TEST_SUITE_END(); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 7ac679359..1350a0d14 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -25,6 +25,24 @@ LUAU_FASTFLAG(LuauReadWritePropertySyntax); TEST_SUITE_BEGIN("TableTests"); +TEST_CASE_FIXTURE(BuiltinsFixture, "LUAU_ASSERT_arg_exprs_doesnt_trigger_assert") +{ + CheckResult result = check(R"( +local FadeValue = {} +function FadeValue.new(finalCallback) + local self = setmetatable({}, FadeValue) + self.finalCallback = finalCallback + return self +end + +function FadeValue:destroy() + self.finalCallback() + self.finalCallback = nil +end +)"); + +} + TEST_CASE_FIXTURE(Fixture, "basic") { CheckResult result = check("local t = {foo = \"bar\", baz = 9, quux = nil}"); @@ -2703,6 +2721,23 @@ TEST_CASE_FIXTURE(Fixture, "tables_get_names_from_their_locals") CHECK_EQ("T", toString(requireType("T"))); } +TEST_CASE_FIXTURE(Fixture, "should_not_unblock_table_type_twice") +{ + ScopedFastFlag sff(FFlag::DebugLuauDeferredConstraintResolution, true); + + check(R"( + local timer = peek(timerQueue) + while timer ~= nil do + if timer.startTime <= currentTime then + timer.isQueued = true + end + timer = peek(timerQueue) + end + )"); + + // Just checking this is enough to satisfy the original bug. +} + TEST_CASE_FIXTURE(Fixture, "generalize_table_argument") { CheckResult result = check(R"( @@ -4176,4 +4211,40 @@ TEST_CASE_FIXTURE(Fixture, "table_writes_introduce_write_properties") "t1 = { read FindFirstChild: (t1, string) -> (a, b...) }" == toString(requireType("oc"))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "tables_can_have_both_metatables_and_indexers") +{ + CheckResult result = check(R"( + local a = {} + a[1] = 5 + a[2] = 17 + + local t = {} + setmetatable(a, t) + + local c = a[1] + print(a[1]) + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK("number" == toString(requireType("c"))); +} + +TEST_CASE_FIXTURE(Fixture, "refined_thing_can_be_an_array") +{ + CheckResult result = check(R"( + function foo(x, y) + if x then + return x[1] + else + return y + end + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); + + CHECK("({a}, a) -> a" == toString(requireType("foo"))); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index e441a6e8a..dfa889340 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -561,6 +561,41 @@ TEST_CASE_FIXTURE(Fixture, "dont_allow_cyclic_unions_to_be_inferred") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "indexing_into_a_cyclic_union_doesnt_crash") +{ + // It shouldn't be possible to craft a cyclic union, but even if we do, we + // shouldn't blow up. + + TypeArena& arena = frontend.globals.globalTypes; + unfreeze(arena); + + TypeId badCyclicUnionTy = arena.freshType(frontend.globals.globalScope.get()); + UnionType u; + + u.options.push_back(badCyclicUnionTy); + u.options.push_back(arena.addType(TableType{{}, TableIndexer{builtinTypes->numberType, builtinTypes->numberType}, TypeLevel{}, frontend.globals.globalScope.get(), TableState::Sealed})); + + asMutable(badCyclicUnionTy)->ty.emplace(std::move(u)); + + frontend.globals.globalScope->exportedTypeBindings["BadCyclicUnion"] = TypeFun{{}, badCyclicUnionTy}; + + freeze(arena); + + CheckResult result = check(R"( + function f(x: BadCyclicUnion) + return x[0] + end + )"); + + // The old solver has a bug: It doesn't consider this goofy thing to be a + // table. It's not really important. What's important is that we don't + // crash, hang, or ICE. + if (FFlag::DebugLuauDeferredConstraintResolution) + LUAU_REQUIRE_NO_ERRORS(result); + else + LUAU_REQUIRE_ERROR_COUNT(1, result); +} + TEST_CASE_FIXTURE(BuiltinsFixture, "table_union_write_indirect") { diff --git a/tests/conformance/debug.lua b/tests/conformance/debug.lua index 0c8cc2d87..e044ea454 100644 --- a/tests/conformance/debug.lua +++ b/tests/conformance/debug.lua @@ -111,4 +111,31 @@ end testlinedefined() +-- don't leave garbage on the other thread +local wrapped1 = coroutine.create(function() + local thread = coroutine.create(function(target) + for i = 1, 100 do pcall(debug.info, target, 0, "llf") end + return 123 + end) + + local success, res = coroutine.resume(thread, coroutine.running()) + assert(success) + assert(res == 123) +end) + +coroutine.resume(wrapped1) + +local wrapped2 = coroutine.create(function() + local thread = coroutine.create(function(target) + for i = 1, 100 do pcall(debug.info, target, 0, "ff") end + return 123 + end) + + local success, res = coroutine.resume(thread, coroutine.running()) + assert(success) + assert(res == 123) +end) + +coroutine.resume(wrapped2) + return 'OK' diff --git a/tests/conformance/native.lua b/tests/conformance/native.lua index 170642131..78692d396 100644 --- a/tests/conformance/native.lua +++ b/tests/conformance/native.lua @@ -427,4 +427,26 @@ end assert(deadStoreChecks1() == 111) +local function extramath1(a) + return type(math.sign(a)) +end + +assert(extramath1(2) == "number") +assert(extramath1("2") == "number") + +local function extramath2(a) + return type(math.modf(a)) +end + +assert(extramath2(2) == "number") +assert(extramath2("2") == "number") + +local function extramath3(a) + local b, c = math.modf(a) + return type(c) +end + +assert(extramath3(2) == "number") +assert(extramath3("2") == "number") + return('OK') diff --git a/tools/faillist.txt b/tools/faillist.txt index c4c06cc14..b657385d6 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_response_perf1 AutocompleteTest.autocomplete_string_singleton_equality AutocompleteTest.do_wrong_compatible_nonself_calls AutocompleteTest.type_correct_suggestion_for_overloads @@ -18,7 +17,6 @@ BuiltinTests.gmatch_capture_types_default_capture BuiltinTests.gmatch_capture_types_parens_in_sets_are_ignored BuiltinTests.gmatch_capture_types_set_containing_lbracket BuiltinTests.gmatch_definition -BuiltinTests.ipairs_iterator_should_infer_types_and_type_check BuiltinTests.os_time_takes_optional_date_table BuiltinTests.select_slightly_out_of_range BuiltinTests.select_way_out_of_range @@ -32,22 +30,6 @@ BuiltinTests.string_format_report_all_type_errors_at_correct_positions BuiltinTests.string_format_use_correct_argument2 BuiltinTests.table_freeze_is_generic BuiltinTests.tonumber_returns_optional_number_type -ControlFlowAnalysis.if_not_x_break_elif_not_y_break -ControlFlowAnalysis.if_not_x_break_elif_not_y_continue -ControlFlowAnalysis.if_not_x_break_elif_not_y_fallthrough_elif_not_z_break -ControlFlowAnalysis.if_not_x_break_elif_rand_break_elif_not_y_break -ControlFlowAnalysis.if_not_x_break_elif_rand_break_elif_not_y_fallthrough -ControlFlowAnalysis.if_not_x_break_if_not_y_break -ControlFlowAnalysis.if_not_x_break_if_not_y_continue -ControlFlowAnalysis.if_not_x_continue_elif_not_y_continue -ControlFlowAnalysis.if_not_x_continue_elif_not_y_fallthrough_elif_not_z_continue -ControlFlowAnalysis.if_not_x_continue_elif_not_y_throw_elif_not_z_fallthrough -ControlFlowAnalysis.if_not_x_continue_elif_rand_continue_elif_not_y_continue -ControlFlowAnalysis.if_not_x_continue_elif_rand_continue_elif_not_y_fallthrough -ControlFlowAnalysis.if_not_x_continue_if_not_y_continue -ControlFlowAnalysis.if_not_x_continue_if_not_y_throw -ControlFlowAnalysis.if_not_x_return_elif_not_y_break -ControlFlowAnalysis.if_not_x_return_elif_not_y_fallthrough_elif_not_z_break ControlFlowAnalysis.tagged_unions DefinitionTests.class_definition_indexer DefinitionTests.class_definition_overload_metamethods @@ -149,7 +131,6 @@ ProvisionalTests.do_not_ice_when_trying_to_pick_first_of_generic_type_pack ProvisionalTests.error_on_eq_metamethod_returning_a_type_other_than_boolean ProvisionalTests.expected_type_should_be_a_helpful_deduction_guide_for_function_calls ProvisionalTests.floating_generics_should_not_be_allowed -ProvisionalTests.free_is_not_bound_to_any ProvisionalTests.free_options_can_be_unified_together ProvisionalTests.free_options_cannot_be_unified_together ProvisionalTests.generic_type_leak_to_module_interface @@ -172,14 +153,10 @@ RefinementTest.discriminate_from_isa_of_x RefinementTest.discriminate_from_truthiness_of_x RefinementTest.discriminate_tag RefinementTest.discriminate_tag_with_implicit_else -RefinementTest.fail_to_refine_a_property_of_subscript_expression -RefinementTest.falsiness_of_TruthyPredicate_narrows_into_nil RefinementTest.globals_can_be_narrowed_too -RefinementTest.impossible_type_narrow_is_not_an_error RefinementTest.index_on_a_refined_property RefinementTest.isa_type_refinement_must_be_known_ahead_of_time RefinementTest.narrow_property_of_a_bounded_variable -RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true RefinementTest.not_t_or_some_prop_of_t RefinementTest.refine_a_param_that_got_resolved_during_constraint_solving_stage RefinementTest.refine_a_property_of_some_global @@ -230,7 +207,7 @@ TableTests.generalize_table_argument TableTests.generic_table_instantiation_potential_regression TableTests.indexer_mismatch TableTests.indexers_get_quantified_too -TableTests.indexing_from_a_table_should_prefer_properties_when_possible +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 @@ -335,10 +312,8 @@ TypeInfer.dont_report_type_errors_within_an_AstStatError TypeInfer.globals TypeInfer.globals2 TypeInfer.globals_are_banned_in_strict_mode -TypeInfer.infer_assignment_value_types TypeInfer.infer_locals_via_assignment_from_its_call_site TypeInfer.infer_through_group_expr -TypeInfer.infer_type_assertion_value_type TypeInfer.no_stack_overflow_from_isoptional TypeInfer.promote_tail_type_packs TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter @@ -352,7 +327,6 @@ 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.intersection_of_any_can_have_props 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 @@ -361,7 +335,6 @@ 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.index_instance_property TypeInferClasses.indexable_classes TypeInferClasses.intersections_of_unions_of_classes TypeInferClasses.optional_class_field_access_error @@ -471,7 +444,6 @@ 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.operator_eq_verifies_types_do_intersect TypeInferOperators.refine_and_or TypeInferOperators.reworked_and TypeInferOperators.reworked_or diff --git a/tools/natvis/VM.natvis b/tools/natvis/VM.natvis index 5d5bf5cb2..59bc43c46 100644 --- a/tools/natvis/VM.natvis +++ b/tools/natvis/VM.natvis @@ -1,7 +1,7 @@ - + nil {(bool)value.b} lightuserdata {(uintptr_t)value.p,h} tag: {extra[0]} @@ -39,7 +39,7 @@ - + nil {(bool)value.b} lightuserdata {(uintptr_t)value.p,h} tag: {extra[0]} @@ -72,12 +72,12 @@ - + {key,na} = {val} --- - + table metatable @@ -103,7 +103,7 @@ - + @@ -131,7 +131,7 @@ - + {c.f,na} {l.p,na} {c} @@ -139,11 +139,11 @@ invalid - + {data,s} - + @@ -163,7 +163,7 @@ =[C] {cl().c.f,na} - + {ci,na} thread @@ -211,7 +211,7 @@ - + {source->data,sb}:{linedefined} function {debugname->data,sb} [{(int)numparams} arg, {(int)nups} upval] {source->data,sb}:{linedefined} [{(int)numparams} arg, {(int)nups} upval] @@ -266,7 +266,7 @@ - + {(lua_Type)tt} diff --git a/tools/test_dcr.py b/tools/test_dcr.py index 0a23587a5..30940c63f 100644 --- a/tools/test_dcr.py +++ b/tools/test_dcr.py @@ -6,9 +6,17 @@ import subprocess as sp import sys import xml.sax as x -import colorama as c -c.init() +try: + import colorama as c +except ImportError: + class c: + class Fore: + RED='' + RESET='' + GREEN='' +else: + c.init() SCRIPT_PATH = os.path.split(sys.argv[0])[0] FAIL_LIST_PATH = os.path.join(SCRIPT_PATH, "faillist.txt")