diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index eb6e18eb8..e7932a35a 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -321,6 +321,11 @@ struct ConstraintGenerator */ void checkFunctionBody(const ScopePtr& scope, AstExprFunction* fn); + // Specializations of 'resolveType' below + TypeId resolveReferenceType(const ScopePtr& scope, AstType* ty, AstTypeReference* ref, bool inTypeArguments, bool replaceErrorWithFresh); + TypeId resolveTableType(const ScopePtr& scope, AstType* ty, AstTypeTable* tab, bool inTypeArguments, bool replaceErrorWithFresh); + TypeId resolveFunctionType(const ScopePtr& scope, AstType* ty, AstTypeFunction* fn, bool inTypeArguments, bool replaceErrorWithFresh); + /** * Resolves a type from its AST annotation. * @param scope the scope that the type annotation appears within. diff --git a/Analysis/include/Luau/Subtyping.h b/Analysis/include/Luau/Subtyping.h index 18217a6b2..09f46c4df 100644 --- a/Analysis/include/Luau/Subtyping.h +++ b/Analysis/include/Luau/Subtyping.h @@ -96,6 +96,22 @@ struct SubtypingEnvironment DenseHashSet upperBound{nullptr}; }; + /* For nested subtyping relationship tests of mapped generic bounds, we keep the outer environment immutable */ + SubtypingEnvironment* parent = nullptr; + + /// Applies `mappedGenerics` to the given type. + /// This is used specifically to substitute for generics in type function instances. + std::optional applyMappedGenerics(NotNull builtinTypes, NotNull arena, TypeId ty); + + const TypeId* tryFindSubstitution(TypeId ty) const; + const SubtypingResult* tryFindSubtypingResult(std::pair subAndSuper) const; + + bool containsMappedType(TypeId ty) const; + bool containsMappedPack(TypePackId tp) const; + + GenericBounds& getMappedTypeBounds(TypeId ty); + TypePackId* getMappedPackBounds(TypePackId tp); + /* * When we encounter a generic over the course of a subtyping test, we need * to tentatively map that generic onto a type on the other side. @@ -112,10 +128,6 @@ struct SubtypingEnvironment DenseHashMap substitutions{nullptr}; DenseHashMap, SubtypingResult, TypePairHash> ephemeralCache{{}}; - - /// Applies `mappedGenerics` to the given type. - /// This is used specifically to substitute for generics in type function instances. - std::optional applyMappedGenerics(NotNull builtinTypes, NotNull arena, TypeId ty); }; struct Subtyping diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index ee865edd1..868e31f12 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -13,7 +13,12 @@ #include #include -LUAU_FASTFLAG(LuauSolverV2); +LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauAutocompleteNewSolverLimit) + +LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) +LUAU_FASTINT(LuauTypeInferIterationLimit) +LUAU_FASTINT(LuauTypeInferRecursionLimit) static const std::unordered_set kStatementStartingKeywords = {"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; @@ -144,6 +149,12 @@ static bool checkTypeMatch(TypeId subTy, TypeId superTy, NotNull scope, T if (FFlag::LuauSolverV2) { + if (FFlag::LuauAutocompleteNewSolverLimit) + { + unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit; + unifierState.counters.iterationLimit = FInt::LuauTypeInferIterationLimit; + } + Subtyping subtyping{builtinTypes, NotNull{typeArena}, NotNull{&normalizer}, NotNull{&iceReporter}}; return subtyping.isSubtype(subTy, superTy, scope).isSubtype; @@ -199,6 +210,9 @@ static TypeCorrectKind checkTypeCorrectKind( { for (TypeId id : itv->parts) { + if (DFInt::LuauTypeSolverRelease >= 644) + id = follow(id); + if (const FunctionType* ftv = get(id); ftv && checkFunctionType(ftv)) { return TypeCorrectKind::CorrectFunctionResult; diff --git a/Analysis/src/ConstraintGenerator.cpp b/Analysis/src/ConstraintGenerator.cpp index face6825c..56a6795aa 100644 --- a/Analysis/src/ConstraintGenerator.cpp +++ b/Analysis/src/ConstraintGenerator.cpp @@ -2949,216 +2949,243 @@ void ConstraintGenerator::checkFunctionBody(const ScopePtr& scope, AstExprFuncti addConstraint(scope, fn->location, PackSubtypeConstraint{builtinTypes->emptyTypePack, scope->returnType}); } -TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh) +TypeId ConstraintGenerator::resolveReferenceType( + const ScopePtr& scope, + AstType* ty, + AstTypeReference* ref, + bool inTypeArguments, + bool replaceErrorWithFresh +) { TypeId result = nullptr; - if (auto ref = ty->as()) + if (FFlag::DebugLuauMagicTypes) { - if (FFlag::DebugLuauMagicTypes) + if (ref->name == "_luau_ice") + ice->ice("_luau_ice encountered", ty->location); + else if (ref->name == "_luau_print") { - if (ref->name == "_luau_ice") - ice->ice("_luau_ice encountered", ty->location); - else if (ref->name == "_luau_print") + if (ref->parameters.size != 1 || !ref->parameters.data[0].type) { - if (ref->parameters.size != 1 || !ref->parameters.data[0].type) - { - reportError(ty->location, GenericError{"_luau_print requires one generic parameter"}); - module->astResolvedTypes[ty] = builtinTypes->errorRecoveryType(); - return builtinTypes->errorRecoveryType(); - } - else - return resolveType(scope, ref->parameters.data[0].type, inTypeArguments); + reportError(ty->location, GenericError{"_luau_print requires one generic parameter"}); + module->astResolvedTypes[ty] = builtinTypes->errorRecoveryType(); + return builtinTypes->errorRecoveryType(); } + else + return resolveType(scope, ref->parameters.data[0].type, inTypeArguments); } + } + + std::optional alias; - std::optional alias; + if (ref->prefix.has_value()) + { + alias = scope->lookupImportedType(ref->prefix->value, ref->name.value); + } + else + { + alias = scope->lookupType(ref->name.value); + } - if (ref->prefix.has_value()) + if (alias.has_value()) + { + // If the alias is not generic, we don't need to set up a blocked + // type and an instantiation constraint. + if (alias.has_value() && alias->typeParams.empty() && alias->typePackParams.empty()) { - alias = scope->lookupImportedType(ref->prefix->value, ref->name.value); + result = alias->type; } else { - alias = scope->lookupType(ref->name.value); - } + std::vector parameters; + std::vector packParameters; - if (alias.has_value()) - { - // If the alias is not generic, we don't need to set up a blocked - // type and an instantiation constraint. - if (alias.has_value() && alias->typeParams.empty() && alias->typePackParams.empty()) - { - result = alias->type; - } - else + for (const AstTypeOrPack& p : ref->parameters) { - std::vector parameters; - std::vector packParameters; - - for (const AstTypeOrPack& p : ref->parameters) + // We do not enforce the ordering of types vs. type packs here; + // that is done in the parser. + if (p.type) { - // We do not enforce the ordering of types vs. type packs here; - // that is done in the parser. - if (p.type) - { - parameters.push_back(resolveType(scope, p.type, /* inTypeArguments */ true)); - } - else if (p.typePack) - { - TypePackId tp = resolveTypePack(scope, p.typePack, /*inTypeArguments*/ true); + parameters.push_back(resolveType(scope, p.type, /* inTypeArguments */ true)); + } + else if (p.typePack) + { + TypePackId tp = resolveTypePack(scope, p.typePack, /*inTypeArguments*/ true); - // If we need more regular types, we can use single element type packs to fill those in - if (parameters.size() < alias->typeParams.size() && size(tp) == 1 && finite(tp) && first(tp)) - parameters.push_back(*first(tp)); - else - packParameters.push_back(tp); - } + // If we need more regular types, we can use single element type packs to fill those in + if (parameters.size() < alias->typeParams.size() && size(tp) == 1 && finite(tp) && first(tp)) + parameters.push_back(*first(tp)); else - { - // This indicates a parser bug: one of these two pointers - // should be set. - LUAU_ASSERT(false); - } + packParameters.push_back(tp); } + else + { + // This indicates a parser bug: one of these two pointers + // should be set. + LUAU_ASSERT(false); + } + } - result = arena->addType(PendingExpansionType{ref->prefix, ref->name, parameters, packParameters}); + result = arena->addType(PendingExpansionType{ref->prefix, ref->name, parameters, packParameters}); - // If we're not in a type argument context, we need to create a constraint that expands this. - // The dispatching of the above constraint will queue up additional constraints for nested - // type function applications. - if (!inTypeArguments) - addConstraint(scope, ty->location, TypeAliasExpansionConstraint{/* target */ result}); - } - } - else - { - result = builtinTypes->errorRecoveryType(); - if (replaceErrorWithFresh) - result = freshType(scope); + // If we're not in a type argument context, we need to create a constraint that expands this. + // The dispatching of the above constraint will queue up additional constraints for nested + // type function applications. + if (!inTypeArguments) + addConstraint(scope, ty->location, TypeAliasExpansionConstraint{/* target */ result}); } } - else if (auto tab = ty->as()) + else { - TableType::Props props; - std::optional indexer; + result = builtinTypes->errorRecoveryType(); + if (replaceErrorWithFresh) + result = freshType(scope); + } - for (const AstTableProp& prop : tab->props) - { - // TODO: Recursion limit. - TypeId propTy = resolveType(scope, prop.type, inTypeArguments); + return result; +} - Property& p = props[prop.name.value]; - p.typeLocation = prop.location; +TypeId ConstraintGenerator::resolveTableType(const ScopePtr& scope, AstType* ty, AstTypeTable* tab, bool inTypeArguments, bool replaceErrorWithFresh) +{ + TableType::Props props; + std::optional indexer; - switch (prop.access) - { - case AstTableAccess::ReadWrite: - p.readTy = propTy; - p.writeTy = propTy; - break; - case AstTableAccess::Read: - p.readTy = propTy; - break; - case AstTableAccess::Write: - reportError(*prop.accessLocation, GenericError{"write keyword is illegal here"}); - p.readTy = propTy; - p.writeTy = propTy; - break; - default: - ice->ice("Unexpected property access " + std::to_string(int(prop.access))); - break; - } - } + for (const AstTableProp& prop : tab->props) + { + TypeId propTy = resolveType(scope, prop.type, inTypeArguments); - if (AstTableIndexer* astIndexer = tab->indexer) + Property& p = props[prop.name.value]; + p.typeLocation = prop.location; + + switch (prop.access) { - if (astIndexer->access == AstTableAccess::Read) - reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"read keyword is illegal here"}); - else if (astIndexer->access == AstTableAccess::Write) - reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"write keyword is illegal here"}); - else if (astIndexer->access == AstTableAccess::ReadWrite) - { - // TODO: Recursion limit. - indexer = TableIndexer{ - resolveType(scope, astIndexer->indexType, inTypeArguments), - resolveType(scope, astIndexer->resultType, inTypeArguments), - }; - } - else - ice->ice("Unexpected property access " + std::to_string(int(astIndexer->access))); + case AstTableAccess::ReadWrite: + p.readTy = propTy; + p.writeTy = propTy; + break; + case AstTableAccess::Read: + p.readTy = propTy; + break; + case AstTableAccess::Write: + reportError(*prop.accessLocation, GenericError{"write keyword is illegal here"}); + p.readTy = propTy; + p.writeTy = propTy; + break; + default: + ice->ice("Unexpected property access " + std::to_string(int(prop.access))); + break; } - - result = arena->addType(TableType{props, indexer, scope->level, scope.get(), TableState::Sealed}); } - else if (auto fn = ty->as()) + + if (AstTableIndexer* astIndexer = tab->indexer) { - // TODO: Recursion limit. - bool hasGenerics = fn->generics.size > 0 || fn->genericPacks.size > 0; - ScopePtr signatureScope = nullptr; + if (astIndexer->access == AstTableAccess::Read) + reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"read keyword is illegal here"}); + else if (astIndexer->access == AstTableAccess::Write) + reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"write keyword is illegal here"}); + else if (astIndexer->access == AstTableAccess::ReadWrite) + { + indexer = TableIndexer{ + resolveType(scope, astIndexer->indexType, inTypeArguments), + resolveType(scope, astIndexer->resultType, inTypeArguments), + }; + } + else + ice->ice("Unexpected property access " + std::to_string(int(astIndexer->access))); + } - std::vector genericTypes; - std::vector genericTypePacks; + return arena->addType(TableType{props, indexer, scope->level, scope.get(), TableState::Sealed}); +} - // If we don't have generics, we do not need to generate a child scope - // for the generic bindings to live on. - if (hasGenerics) - { - signatureScope = childScope(fn, scope); +TypeId ConstraintGenerator::resolveFunctionType( + const ScopePtr& scope, + AstType* ty, + AstTypeFunction* fn, + bool inTypeArguments, + bool replaceErrorWithFresh +) +{ + bool hasGenerics = fn->generics.size > 0 || fn->genericPacks.size > 0; + ScopePtr signatureScope = nullptr; + + std::vector genericTypes; + std::vector genericTypePacks; - std::vector> genericDefinitions = createGenerics(signatureScope, fn->generics); - std::vector> genericPackDefinitions = createGenericPacks(signatureScope, fn->genericPacks); + // If we don't have generics, we do not need to generate a child scope + // for the generic bindings to live on. + if (hasGenerics) + { + signatureScope = childScope(fn, scope); - for (const auto& [name, g] : genericDefinitions) - { - genericTypes.push_back(g.ty); - } + std::vector> genericDefinitions = createGenerics(signatureScope, fn->generics); + std::vector> genericPackDefinitions = createGenericPacks(signatureScope, fn->genericPacks); - for (const auto& [name, g] : genericPackDefinitions) - { - genericTypePacks.push_back(g.tp); - } + for (const auto& [name, g] : genericDefinitions) + { + genericTypes.push_back(g.ty); } - else + + for (const auto& [name, g] : genericPackDefinitions) { - // To eliminate the need to branch on hasGenerics below, we say that - // the signature scope is the parent scope if we don't have - // generics. - signatureScope = scope; + genericTypePacks.push_back(g.tp); } + } + else + { + // To eliminate the need to branch on hasGenerics below, we say that + // the signature scope is the parent scope if we don't have + // generics. + signatureScope = scope; + } - TypePackId argTypes = resolveTypePack(signatureScope, fn->argTypes, inTypeArguments, replaceErrorWithFresh); - TypePackId returnTypes = resolveTypePack(signatureScope, fn->returnTypes, inTypeArguments, replaceErrorWithFresh); + TypePackId argTypes = resolveTypePack(signatureScope, fn->argTypes, inTypeArguments, replaceErrorWithFresh); + TypePackId returnTypes = resolveTypePack(signatureScope, fn->returnTypes, inTypeArguments, replaceErrorWithFresh); - // TODO: FunctionType needs a pointer to the scope so that we know - // how to quantify/instantiate it. - FunctionType ftv{TypeLevel{}, scope.get(), {}, {}, argTypes, returnTypes}; - ftv.isCheckedFunction = fn->isCheckedFunction(); + // TODO: FunctionType needs a pointer to the scope so that we know + // how to quantify/instantiate it. + FunctionType ftv{TypeLevel{}, scope.get(), {}, {}, argTypes, returnTypes}; + ftv.isCheckedFunction = fn->isCheckedFunction(); - // This replicates the behavior of the appropriate FunctionType - // constructors. - ftv.generics = std::move(genericTypes); - ftv.genericPacks = std::move(genericTypePacks); + // This replicates the behavior of the appropriate FunctionType + // constructors. + ftv.generics = std::move(genericTypes); + ftv.genericPacks = std::move(genericTypePacks); - ftv.argNames.reserve(fn->argNames.size); - for (const auto& el : fn->argNames) + ftv.argNames.reserve(fn->argNames.size); + for (const auto& el : fn->argNames) + { + if (el) { - if (el) - { - const auto& [name, location] = *el; - ftv.argNames.push_back(FunctionArgument{name.value, location}); - } - else - { - ftv.argNames.push_back(std::nullopt); - } + const auto& [name, location] = *el; + ftv.argNames.push_back(FunctionArgument{name.value, location}); + } + else + { + ftv.argNames.push_back(std::nullopt); } + } + + return arena->addType(std::move(ftv)); +} + +TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh) +{ + TypeId result = nullptr; - result = arena->addType(std::move(ftv)); + if (auto ref = ty->as()) + { + result = resolveReferenceType(scope, ty, ref, inTypeArguments, replaceErrorWithFresh); + } + else if (auto tab = ty->as()) + { + result = resolveTableType(scope, ty, tab, inTypeArguments, replaceErrorWithFresh); + } + else if (auto fn = ty->as()) + { + result = resolveFunctionType(scope, ty, fn, inTypeArguments, replaceErrorWithFresh); } else if (auto tof = ty->as()) { - // TODO: Recursion limit. TypeId exprType = check(scope, tof->expr).ty; result = exprType; } @@ -3167,7 +3194,6 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool std::vector parts; for (AstType* part : unionAnnotation->types) { - // TODO: Recursion limit. parts.push_back(resolveType(scope, part, inTypeArguments)); } @@ -3178,7 +3204,6 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool std::vector parts; for (AstType* part : intersectionAnnotation->types) { - // TODO: Recursion limit. parts.push_back(resolveType(scope, part, inTypeArguments)); } diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index ae02c60ae..f7c4fb5eb 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -27,10 +27,14 @@ #include #include -LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false); +LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverIncludeDependencies, false) -LUAU_FASTFLAGVARIABLE(DebugLuauLogBindings, false); -LUAU_FASTINTVARIABLE(LuauSolverRecursionLimit, 500); +LUAU_FASTFLAGVARIABLE(DebugLuauLogBindings, false) +LUAU_FASTINTVARIABLE(LuauSolverRecursionLimit, 500) + +// The default value here is 643 because the first release in which this was implemented is 644, +// and actively we want new changes to be off by default until they're enabled consciously. +LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeSolverRelease, 643) namespace Luau { diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 2db2f40c6..c768f02c0 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -21,11 +21,12 @@ LUAU_FASTFLAGVARIABLE(LuauNormalizeNotUnknownIntersection, false); LUAU_FASTFLAGVARIABLE(LuauFixReduceStackPressure, false); LUAU_FASTFLAGVARIABLE(LuauFixCyclicTablesBlowingStack, false); -// This could theoretically be 2000 on amd64, but x86 requires this. -LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200); LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000); LUAU_FASTFLAG(LuauSolverV2); +LUAU_FASTFLAGVARIABLE(LuauUseNormalizeIntersectionLimit, false) +LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200) + static bool fixReduceStackPressure() { return FFlag::LuauFixReduceStackPressure || FFlag::LuauSolverV2; @@ -3035,6 +3036,14 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor return unionNormals(here, there, ignoreSmallerTyvars); } + if (FFlag::LuauUseNormalizeIntersectionLimit) + { + // Limit based on worst-case expansion of the table intersection + // This restriction can be relaxed when table intersection simplification is improved + if (here.tables.size() * there.tables.size() >= size_t(FInt::LuauNormalizeIntersectionLimit)) + return NormalizationResult::HitLimits; + } + here.booleans = intersectionOfBools(here.booleans, there.booleans); intersectClasses(here.classes, there.classes); diff --git a/Analysis/src/Subtyping.cpp b/Analysis/src/Subtyping.cpp index ee199b66c..b13a2327b 100644 --- a/Analysis/src/Subtyping.cpp +++ b/Analysis/src/Subtyping.cpp @@ -5,6 +5,7 @@ #include "Luau/Common.h" #include "Luau/Error.h" #include "Luau/Normalize.h" +#include "Luau/RecursionCounter.h" #include "Luau/Scope.h" #include "Luau/StringUtils.h" #include "Luau/Substitution.h" @@ -21,6 +22,8 @@ #include LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity, false); +LUAU_FASTFLAGVARIABLE(LuauAutocompleteNewSolverLimit, false); +LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) namespace Luau { @@ -264,50 +267,86 @@ struct ApplyMappedGenerics : Substitution NotNull builtinTypes; NotNull arena; - MappedGenerics& mappedGenerics; - MappedGenericPacks& mappedGenericPacks; + SubtypingEnvironment& env; + MappedGenerics& mappedGenerics_DEPRECATED; + MappedGenericPacks& mappedGenericPacks_DEPRECATED; ApplyMappedGenerics( NotNull builtinTypes, NotNull arena, + SubtypingEnvironment& env, MappedGenerics& mappedGenerics, MappedGenericPacks& mappedGenericPacks ) : Substitution(TxnLog::empty(), arena) , builtinTypes(builtinTypes) , arena(arena) - , mappedGenerics(mappedGenerics) - , mappedGenericPacks(mappedGenericPacks) + , env(env) + , mappedGenerics_DEPRECATED(mappedGenerics) + , mappedGenericPacks_DEPRECATED(mappedGenericPacks) { } bool isDirty(TypeId ty) override { - return mappedGenerics.contains(ty); + if (DFInt::LuauTypeSolverRelease >= 644) + return env.containsMappedType(ty); + else + return mappedGenerics_DEPRECATED.contains(ty); } bool isDirty(TypePackId tp) override { - return mappedGenericPacks.contains(tp); + if (DFInt::LuauTypeSolverRelease >= 644) + return env.containsMappedPack(tp); + else + return mappedGenericPacks_DEPRECATED.contains(tp); } TypeId clean(TypeId ty) override { - const auto& bounds = mappedGenerics[ty]; + if (DFInt::LuauTypeSolverRelease >= 644) + { + const auto& bounds = env.getMappedTypeBounds(ty); + + if (bounds.upperBound.empty()) + return builtinTypes->unknownType; + + if (bounds.upperBound.size() == 1) + return *begin(bounds.upperBound); + + return arena->addType(IntersectionType{std::vector(begin(bounds.upperBound), end(bounds.upperBound))}); + } + else + { + const auto& bounds = mappedGenerics_DEPRECATED[ty]; - if (bounds.upperBound.empty()) - return builtinTypes->unknownType; + if (bounds.upperBound.empty()) + return builtinTypes->unknownType; - if (bounds.upperBound.size() == 1) - return *begin(bounds.upperBound); + if (bounds.upperBound.size() == 1) + return *begin(bounds.upperBound); - return arena->addType(IntersectionType{std::vector(begin(bounds.upperBound), end(bounds.upperBound))}); + return arena->addType(IntersectionType{std::vector(begin(bounds.upperBound), end(bounds.upperBound))}); + } } TypePackId clean(TypePackId tp) override { - return mappedGenericPacks[tp]; + if (DFInt::LuauTypeSolverRelease >= 644) + { + if (auto it = env.getMappedPackBounds(tp)) + return *it; + + // Clean is only called when isDirty found a pack bound + LUAU_ASSERT(!"Unreachable"); + return nullptr; + } + else + { + return mappedGenericPacks_DEPRECATED[tp]; + } } bool ignoreChildren(TypeId ty) override @@ -325,10 +364,78 @@ struct ApplyMappedGenerics : Substitution std::optional SubtypingEnvironment::applyMappedGenerics(NotNull builtinTypes, NotNull arena, TypeId ty) { - ApplyMappedGenerics amg{builtinTypes, arena, mappedGenerics, mappedGenericPacks}; + ApplyMappedGenerics amg{builtinTypes, arena, *this, mappedGenerics, mappedGenericPacks}; return amg.substitute(ty); } +const TypeId* SubtypingEnvironment::tryFindSubstitution(TypeId ty) const +{ + if (auto it = substitutions.find(ty)) + return it; + + if (parent) + return parent->tryFindSubstitution(ty); + + return nullptr; +} + +const SubtypingResult* SubtypingEnvironment::tryFindSubtypingResult(std::pair subAndSuper) const +{ + if (auto it = ephemeralCache.find(subAndSuper)) + return it; + + if (parent) + return parent->tryFindSubtypingResult(subAndSuper); + + return nullptr; +} + +bool SubtypingEnvironment::containsMappedType(TypeId ty) const +{ + if (mappedGenerics.contains(ty)) + return true; + + if (parent) + return parent->containsMappedType(ty); + + return false; +} + +bool SubtypingEnvironment::containsMappedPack(TypePackId tp) const +{ + if (mappedGenericPacks.contains(tp)) + return true; + + if (parent) + return parent->containsMappedPack(tp); + + return false; +} + +SubtypingEnvironment::GenericBounds& SubtypingEnvironment::getMappedTypeBounds(TypeId ty) +{ + if (auto it = mappedGenerics.find(ty)) + return *it; + + if (parent) + return parent->getMappedTypeBounds(ty); + + LUAU_ASSERT(!"Use containsMappedType before asking for bounds!"); + return mappedGenerics[ty]; +} + +TypePackId* SubtypingEnvironment::getMappedPackBounds(TypePackId tp) +{ + if (auto it = mappedGenericPacks.find(tp)) + return it; + + if (parent) + return parent->getMappedPackBounds(tp); + + // This fallback is reachable in valid cases, unlike the final part of getMappedTypeBounds + return nullptr; +} + Subtyping::Subtyping( NotNull builtinTypes, NotNull typeArena, @@ -379,10 +486,23 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy, NotNull= 644) + { + SubtypingEnvironment boundsEnv; + boundsEnv.parent = &env; + SubtypingResult boundsResult = isCovariantWith(boundsEnv, lowerBound, upperBound, scope); + boundsResult.reasoning.clear(); + + result.andAlso(boundsResult); + } + else + { + SubtypingResult boundsResult = isCovariantWith(env, lowerBound, upperBound, scope); + boundsResult.reasoning.clear(); + + result.andAlso(boundsResult); + } } /* TODO: We presently don't store subtype test results in the persistent @@ -442,20 +562,36 @@ struct SeenSetPopper SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy, NotNull scope) { + std::optional rc; + + if (FFlag::LuauAutocompleteNewSolverLimit) + { + UnifierCounters& counters = normalizer->sharedState->counters; + rc.emplace(&counters.recursionCount); + + if (counters.recursionLimit > 0 && counters.recursionLimit < counters.recursionCount) + { + SubtypingResult result; + result.normalizationTooComplex = true; + return result; + } + } + subTy = follow(subTy); superTy = follow(superTy); - if (TypeId* subIt = env.substitutions.find(subTy); subIt && *subIt) + if (const TypeId* subIt = (DFInt::LuauTypeSolverRelease >= 644 ? env.tryFindSubstitution(subTy) : env.substitutions.find(subTy)); subIt && *subIt) subTy = *subIt; - if (TypeId* superIt = env.substitutions.find(superTy); superIt && *superIt) + if (const TypeId* superIt = (DFInt::LuauTypeSolverRelease >= 644 ? env.tryFindSubstitution(superTy) : env.substitutions.find(superTy)); + superIt && *superIt) superTy = *superIt; - SubtypingResult* cachedResult = resultCache.find({subTy, superTy}); + const SubtypingResult* cachedResult = resultCache.find({subTy, superTy}); if (cachedResult) return *cachedResult; - cachedResult = env.ephemeralCache.find({subTy, superTy}); + cachedResult = DFInt::LuauTypeSolverRelease >= 644 ? env.tryFindSubtypingResult({subTy, superTy}) : env.ephemeralCache.find({subTy, superTy}); if (cachedResult) return *cachedResult; @@ -700,7 +836,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId std::vector headSlice(begin(superHead), begin(superHead) + headSize); TypePackId superTailPack = arena->addTypePack(std::move(headSlice), superTail); - if (TypePackId* other = env.mappedGenericPacks.find(*subTail)) + if (TypePackId* other = + (DFInt::LuauTypeSolverRelease >= 644 ? env.getMappedPackBounds(*subTail) : env.mappedGenericPacks.find(*subTail))) // TODO: TypePath can't express "slice of a pack + its tail". results.push_back(isCovariantWith(env, *other, superTailPack, scope).withSubComponent(TypePath::PackField::Tail)); else @@ -755,7 +892,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId std::vector headSlice(begin(subHead), begin(subHead) + headSize); TypePackId subTailPack = arena->addTypePack(std::move(headSlice), subTail); - if (TypePackId* other = env.mappedGenericPacks.find(*superTail)) + if (TypePackId* other = + (DFInt::LuauTypeSolverRelease >= 644 ? env.getMappedPackBounds(*superTail) : env.mappedGenericPacks.find(*superTail))) // TODO: TypePath can't express "slice of a pack + its tail". results.push_back(isContravariantWith(env, subTailPack, *other, scope).withSuperComponent(TypePath::PackField::Tail)); else @@ -1688,6 +1826,12 @@ bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypeId subTy, TypeId supe if (!get(subTy)) return false; + if (DFInt::LuauTypeSolverRelease >= 644) + { + if (!env.mappedGenerics.find(subTy) && env.containsMappedType(subTy)) + iceReporter->ice("attempting to modify bounds of a potentially visited generic"); + } + env.mappedGenerics[subTy].upperBound.insert(superTy); } else @@ -1695,6 +1839,12 @@ bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypeId subTy, TypeId supe if (!get(superTy)) return false; + if (DFInt::LuauTypeSolverRelease >= 644) + { + if (!env.mappedGenerics.find(superTy) && env.containsMappedType(superTy)) + iceReporter->ice("attempting to modify bounds of a potentially visited generic"); + } + env.mappedGenerics[superTy].lowerBound.insert(subTy); } @@ -1740,7 +1890,7 @@ bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypePackId subTp, TypePac if (!get(subTp)) return false; - if (TypePackId* m = env.mappedGenericPacks.find(subTp)) + if (TypePackId* m = (DFInt::LuauTypeSolverRelease >= 644 ? env.getMappedPackBounds(subTp) : env.mappedGenericPacks.find(subTp))) return *m == superTp; env.mappedGenericPacks[subTp] = superTp; diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 7023fba91..ed66453dd 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -31,6 +31,7 @@ #include LUAU_FASTFLAG(DebugLuauMagicTypes) +LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) namespace Luau { @@ -3012,11 +3013,20 @@ PropertyType TypeChecker2::hasIndexTypeFromType( if (tt->indexer) { TypeId indexType = follow(tt->indexer->indexType); - if (isPrim(indexType, PrimitiveType::String)) - return {NormalizationResult::True, {tt->indexer->indexResultType}}; - // If the indexer looks like { [any] : _} - the prop lookup should be allowed! - else if (get(indexType) || get(indexType)) - return {NormalizationResult::True, {tt->indexer->indexResultType}}; + if (DFInt::LuauTypeSolverRelease >= 644) + { + TypeId givenType = module->internalTypes.addType(SingletonType{StringSingleton{prop}}); + if (isSubtype(givenType, indexType, NotNull{module->getModuleScope().get()}, builtinTypes, *ice)) + return {NormalizationResult::True, {tt->indexer->indexResultType}}; + } + else + { + if (isPrim(indexType, PrimitiveType::String)) + return {NormalizationResult::True, {tt->indexer->indexResultType}}; + // If the indexer looks like { [any] : _} - the prop lookup should be allowed! + else if (get(indexType) || get(indexType)) + return {NormalizationResult::True, {tt->indexer->indexResultType}}; + } } diff --git a/Analysis/src/TypeFunction.cpp b/Analysis/src/TypeFunction.cpp index 9ae57fd10..31154cc24 100644 --- a/Analysis/src/TypeFunction.cpp +++ b/Analysis/src/TypeFunction.cpp @@ -37,6 +37,8 @@ LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyUseGuesserDepth, -1); LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies, false); +LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) + namespace Luau { @@ -669,8 +671,16 @@ TypeFunctionReductionResult lenTypeFunction( if (normTy->hasTopTable() || get(normalizedOperand)) return {ctx->builtins->numberType, false, {}, {}}; - if (auto result = tryDistributeTypeFunctionApp(notTypeFunction, instance, typeParams, packParams, ctx)) - return *result; + if (DFInt::LuauTypeSolverRelease >= 644) + { + if (auto result = tryDistributeTypeFunctionApp(lenTypeFunction, instance, typeParams, packParams, ctx)) + return *result; + } + else + { + if (auto result = tryDistributeTypeFunctionApp(notTypeFunction, instance, typeParams, packParams, ctx)) + return *result; + } // findMetatableEntry demands the ability to emit errors, so we must give it // the necessary state to do that, even if we intend to just eat the errors. @@ -758,8 +768,16 @@ TypeFunctionReductionResult unmTypeFunction( if (normTy->isExactlyNumber()) return {ctx->builtins->numberType, false, {}, {}}; - if (auto result = tryDistributeTypeFunctionApp(notTypeFunction, instance, typeParams, packParams, ctx)) - return *result; + if (DFInt::LuauTypeSolverRelease >= 644) + { + if (auto result = tryDistributeTypeFunctionApp(unmTypeFunction, instance, typeParams, packParams, ctx)) + return *result; + } + else + { + if (auto result = tryDistributeTypeFunctionApp(notTypeFunction, instance, typeParams, packParams, ctx)) + return *result; + } // findMetatableEntry demands the ability to emit errors, so we must give it // the necessary state to do that, even if we intend to just eat the errors. @@ -2208,9 +2226,7 @@ TypeFunctionReductionResult indexFunctionImpl( TypeId indexerTy = follow(typeParams.at(1)); if (isPending(indexerTy, ctx->solver)) - { return {std::nullopt, false, {indexerTy}, {}}; - } std::shared_ptr indexerNormTy = ctx->normalizer->normalize(indexerTy); diff --git a/tests/Autocomplete.test.cpp b/tests/Autocomplete.test.cpp index 7f020b18b..82e5c252f 100644 --- a/tests/Autocomplete.test.cpp +++ b/tests/Autocomplete.test.cpp @@ -15,6 +15,9 @@ LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2) LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) +LUAU_FASTFLAG(LuauAutocompleteNewSolverLimit) +LUAU_FASTINT(LuauTypeInferRecursionLimit) +LUAU_FASTFLAG(LuauUseNormalizeIntersectionLimit) using namespace Luau; @@ -3815,6 +3818,40 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_response_perf1" * doctest::timeout(0. CHECK(ac.entryMap.count("Instance")); } +TEST_CASE_FIXTURE(ACFixture, "autocomplete_subtyping_recursion_limit") +{ + // TODO: in old solver, type resolve can't handle the type in this test without a stack overflow + if (!FFlag::LuauSolverV2) + return; + + ScopedFastFlag luauAutocompleteNewSolverLimit{FFlag::LuauAutocompleteNewSolverLimit, true}; + ScopedFastInt luauTypeInferRecursionLimit{FInt::LuauTypeInferRecursionLimit, 10}; + + const int parts = 100; + std::string source; + + source += "function f()\n"; + + std::string prefix; + for (int i = 0; i < parts; i++) + formatAppend(prefix, "(nil|({a%d:number}&", i); + formatAppend(prefix, "(nil|{a%d:number})", parts); + for (int i = 0; i < parts; i++) + formatAppend(prefix, "))"); + + source += "local x1 : " + prefix + "\n"; + source += "local y : {a1:number} = x@1\n"; + + source += "end\n"; + + check(source); + + auto ac = autocomplete('1'); + + CHECK(ac.entryMap.count("true")); + CHECK(ac.entryMap.count("x1")); +} + TEST_CASE_FIXTURE(ACFixture, "strict_mode_force") { check(R"( diff --git a/tests/Fixture.cpp b/tests/Fixture.cpp index 0b0e1b7c9..8f918f0c0 100644 --- a/tests/Fixture.cpp +++ b/tests/Fixture.cpp @@ -16,6 +16,7 @@ #include "doctest.h" #include +#include #include #include #include @@ -27,6 +28,7 @@ LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(DebugLuauFreezeArena); LUAU_FASTFLAG(DebugLuauLogSolverToJsonFile) LUAU_FASTFLAG(LuauDCRMagicFunctionTypeChecker); +LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) extern std::optional randomSeed; // tests/main.cpp @@ -152,8 +154,12 @@ const Config& TestConfigResolver::getConfig(const ModuleName& name) const Fixture::Fixture(bool freeze, bool prepareAutocomplete) : sff_DebugLuauFreezeArena(FFlag::DebugLuauFreezeArena, freeze) - // In tests, we *always* want to register the extra magic functions for typechecking `string.format`. , sff_LuauDCRMagicFunctionTypeChecker(FFlag::LuauDCRMagicFunctionTypeChecker, true) + // The first value of LuauTypeSolverRelease was 643, so as long as this is + // some number greater than 900 (5 years worth of releases), all tests that + // run under the new solver will run against all of the changes guarded by + // this flag. + , sff_LuauTypeSolverRelease(DFInt::LuauTypeSolverRelease, std::numeric_limits::max()) , frontend( &fileResolver, &configResolver, diff --git a/tests/Fixture.h b/tests/Fixture.h index f50431f38..9b2db5b85 100644 --- a/tests/Fixture.h +++ b/tests/Fixture.h @@ -98,9 +98,37 @@ struct Fixture TypeId requireTypeAlias(const std::string& name); TypeId requireExportedType(const ModuleName& moduleName, const std::string& name); + // TODO: Should this be in a container of some kind? Seems a little silly + // to have a bunch of flags sitting on the text fixture. + + // We have a couple flags that are OK to set for all tests and, in some + // cases, cannot easily be flipped on or off on a per-test basis. For these + // we set them as part of constructing the test fixture. + + /* From the original commit: + * + * > This enables arena freezing for all but two unit tests. Arena + * > freezing marks the `TypeArena`'s underlying memory as read-only, + * > raising an access violation whenever you mutate it. This is useful + * > for tracking down violations of Luau's memory model. + */ ScopedFastFlag sff_DebugLuauFreezeArena; + + /* Magic typechecker functions for the new solver are initialized when the + * typechecker frontend is initialized, which is done at the beginning of + * the test: we set this flag as part of the fixture as we always want to + * enable the magic functions for, say, `string.format`. + */ ScopedFastFlag sff_LuauDCRMagicFunctionTypeChecker; + /* While the new solver is being rolled out we are using a monotonically + * increasing version number to track new changes, we just set it to a + * sufficiently high number in tests to ensure that any guards in prod + * code pass in tests (so we don't accidentally reintroduce a bug before + * it's unflagged). + */ + ScopedFastInt sff_LuauTypeSolverRelease; + TestFileResolver fileResolver; TestConfigResolver configResolver; NullModuleResolver moduleResolver; diff --git a/tests/TypeFunction.test.cpp b/tests/TypeFunction.test.cpp index 2b7b40a3c..d3732d606 100644 --- a/tests/TypeFunction.test.cpp +++ b/tests/TypeFunction.test.cpp @@ -939,14 +939,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "index_wait_for_pending_no_crash") Exp = 0, MaxExp = 100 } - type Keys = index> - -- This function makes it think that there's going to be a pending expansion local function UpdateData(key: Keys, value) PlayerData[key] = value end - UpdateData("Coins", 2) )"); diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index ffd01f245..a84a0206c 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -1125,7 +1125,11 @@ TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments") TypeMismatch* tm = get(result.errors[0]); REQUIRE(tm); CHECK_EQ("((number) -> number, string) -> number", toString(tm->wantedType)); - if (FFlag::LuauInstantiateInSubtyping) + // The new solver does not attempt to instantiate generics here, so if + // either the instantiate in subtyping flag _or_ the new solver flags + // are set, assert that we're getting back the original generic + // function definition. + if (FFlag::LuauInstantiateInSubtyping || FFlag::LuauSolverV2) CHECK_EQ("((a) -> (b...), a) -> (b...)", toString(tm->givenType)); else CHECK_EQ("((number) -> number, number) -> number", toString(tm->givenType)); @@ -1148,7 +1152,11 @@ TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments2") TypeMismatch* tm = get(result.errors[0]); REQUIRE(tm); CHECK_EQ("(string, string) -> number", toString(tm->wantedType)); - if (FFlag::LuauInstantiateInSubtyping) + // The new solver does not attempt to instantiate generics here, so if + // either the instantiate in subtyping flag _or_ the new solver flags + // are set, assert that we're getting back the original generic + // function definition. + if (FFlag::LuauInstantiateInSubtyping || FFlag::LuauSolverV2) CHECK_EQ("((a) -> (b...), a) -> (b...)", toString(tm->givenType)); else CHECK_EQ("((string) -> number, string) -> number", toString(*tm->givenType)); @@ -1587,4 +1595,31 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "generic_type_functions_work_in_subtyping") LUAU_REQUIRE_NO_ERRORS(result); } +TEST_CASE_FIXTURE(Fixture, "generic_type_subtyping_nested_bounds_with_new_mappings") +{ + // Test shows how going over mapped generics in a subtyping check can generate more mapped generics when making a subtyping check between bounds. + // It has previously caused iterator invalidation in the new solver, but this specific test doesn't trigger a UAF, only shows an example. + if (!FFlag::LuauSolverV2) + return; + + CheckResult result = check(R"( +type Dispatch = (A) -> () +type BasicStateAction = ((S) -> S) | S + +function updateReducer(reducer: (S, A) -> S, initialArg: I, init: ((I) -> S)?): (S, Dispatch) + return 1 :: any +end + +function basicStateReducer(state: S, action: BasicStateAction): S + return action +end + +function updateState(initialState: (() -> S) | S): (S, Dispatch>) + return updateReducer(basicStateReducer, initialState) +end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index d8bf91525..18c3410d3 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -1576,7 +1576,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "compare_singleton_string_to_string") end )"); - if (FFlag::LuauRemoveBadRelationalOperatorWarning) + // There is a flag to gate turning this off, and this warning is not + // implemented in the new solver, so assert there are no errors. + if (FFlag::LuauRemoveBadRelationalOperatorWarning || FFlag::LuauSolverV2) LUAU_REQUIRE_NO_ERRORS(result); else LUAU_REQUIRE_ERROR_COUNT(1, result); diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index 9c26f1652..417973844 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -8,6 +8,7 @@ #include "doctest.h" LUAU_FASTFLAG(LuauSolverV2) +LUAU_FASTFLAG(LuauUseNormalizeIntersectionLimit) using namespace Luau; @@ -2324,4 +2325,50 @@ end) )")); } +TEST_CASE_FIXTURE(Fixture, "refinements_table_intersection_limits" * doctest::timeout(0.5)) +{ + ScopedFastFlag LuauUseNormalizeIntersectionLimit{FFlag::LuauUseNormalizeIntersectionLimit, true}; + + CheckResult result = check(R"( +--!strict +type Dir = { + a: number?, b: number?, c: number?, d: number?, e: number?, f: number?, + g: number?, h: number?, i: number?, j: number?, k: number?, l: number?, + m: number?, n: number?, o: number?, p: number?, q: number?, r: number?, +} + +local function test(dirs: {Dir}) + for k, dir in dirs + local success, message = pcall(function() + assert(dir.a == nil or type(dir.a) == "number") + assert(dir.b == nil or type(dir.b) == "number") + assert(dir.c == nil or type(dir.c) == "number") + assert(dir.d == nil or type(dir.d) == "number") + assert(dir.e == nil or type(dir.e) == "number") + assert(dir.f == nil or type(dir.f) == "number") + assert(dir.g == nil or type(dir.g) == "number") + assert(dir.h == nil or type(dir.h) == "number") + assert(dir.i == nil or type(dir.i) == "number") + assert(dir.j == nil or type(dir.j) == "number") + assert(dir.k == nil or type(dir.k) == "number") + assert(dir.l == nil or type(dir.l) == "number") + assert(dir.m == nil or type(dir.m) == "number") + assert(dir.n == nil or type(dir.n) == "number") + assert(dir.o == nil or type(dir.o) == "number") + assert(dir.p == nil or type(dir.p) == "number") + assert(dir.q == nil or type(dir.q) == "number") + assert(dir.r == nil or type(dir.r) == "number") + assert(dir.t == nil or type(dir.t) == "number") + assert(dir.u == nil or type(dir.u) == "number") + assert(dir.v == nil or type(dir.v) == "number") + local checkpoint = dir + + checkpoint.w = 1 + end) + assert(success) + end +end + )"); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.singletons.test.cpp b/tests/TypeInfer.singletons.test.cpp index 43b1305e2..e2efc8fbd 100644 --- a/tests/TypeInfer.singletons.test.cpp +++ b/tests/TypeInfer.singletons.test.cpp @@ -334,6 +334,27 @@ TEST_CASE_FIXTURE(Fixture, "table_properties_alias_or_parens_is_indexer") CHECK_EQ("Cannot have more than one table indexer", toString(result.errors[0])); } +TEST_CASE_FIXTURE(Fixture, "indexer_can_be_union_of_singletons") +{ + if (!FFlag::LuauSolverV2) + return; + + CheckResult result = check(R"( + type Target = "A" | "B" + + type Test = {[Target]: number} + + local test: Test = {} + + test.A = 2 + test.C = 4 + )"); + + LUAU_REQUIRE_ERROR_COUNT(1, result); + + CHECK(8 == result.errors[0].location.begin.line); +} + TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes") { CheckResult result = check(R"( diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 9d76e7bd7..46c1d1c1c 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -21,6 +21,7 @@ LUAU_FASTFLAG(LuauFixIndexerSubtypingOrdering) LUAU_FASTFLAG(LuauAcceptIndexingTableUnionsIntersections) LUAU_DYNAMIC_FASTFLAG(LuauImproveNonFunctionCallError) +LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) TEST_SUITE_BEGIN("TableTests"); @@ -2653,12 +2654,15 @@ local y = #x TEST_CASE_FIXTURE(Fixture, "length_operator_union_errors") { + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + CheckResult result = check(R"( local x: {number} | number | string local y = #x )"); - LUAU_REQUIRE_ERROR_COUNT(1, result); + // CLI-119936: This shouldn't double error but does under the new solver. + LUAU_REQUIRE_ERROR_COUNT(2, result); } TEST_CASE_FIXTURE(BuiltinsFixture, "dont_hang_when_trying_to_look_up_in_cyclic_metatable_index") @@ -3261,22 +3265,22 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_call_metamethod_must_be_callable") LUAU_REQUIRE_ERROR_COUNT(1, result); - if (FFlag::LuauSolverV2) - { - if (DFFlag::LuauImproveNonFunctionCallError) - CHECK("Cannot call a value of type a" == toString(result.errors[0])); - else - CHECK("Cannot call non-function { @metatable { __call: number }, { } }" == toString(result.errors[0])); - } - else + if (!FFlag::LuauSolverV2) { TypeError e{ Location{{5, 20}, {5, 21}}, CannotCallNonFunction{builtinTypes->numberType}, }; - CHECK(result.errors[0] == e); } + else if (DFFlag::LuauImproveNonFunctionCallError) + { + CHECK("Cannot call a value of type a" == toString(result.errors[0])); + } + else + { + CHECK("Cannot call non-function a" == toString(result.errors[0])); + } } TEST_CASE_FIXTURE(BuiltinsFixture, "table_call_metamethod_generic") @@ -4832,4 +4836,19 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "indexing_branching_table2") CHECK("any" == toString(requireType("test2"))); } +TEST_CASE_FIXTURE(BuiltinsFixture, "length_of_array_is_number") +{ + CheckResult result = check(R"( + local function TestFunc(ranges: {number}): number + if true then + ranges = {} :: {number} + end + local numRanges: number = #ranges + return numRanges + end + )"); + + LUAU_REQUIRE_NO_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index 6cdec4aff..0303f546f 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -419,6 +419,9 @@ TEST_CASE_FIXTURE(Fixture, "optional_assignment_errors_2") TEST_CASE_FIXTURE(Fixture, "optional_length_error") { + + ScopedFastFlag _{FFlag::LuauSolverV2, true}; + CheckResult result = check(R"( type A = {number} function f(a: A?) @@ -426,8 +429,10 @@ TEST_CASE_FIXTURE(Fixture, "optional_length_error") end )"); - LUAU_REQUIRE_ERROR_COUNT(1, result); - CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0])); + // CLI-119936: This shouldn't double error but does under the new solver. + LUAU_REQUIRE_ERROR_COUNT(2, result); + CHECK_EQ("Operator '#' could not be applied to operand of type A?; there is no corresponding overload for __len", toString(result.errors[0])); + CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[1])); } TEST_CASE_FIXTURE(Fixture, "optional_missing_key_error_details") @@ -638,8 +643,9 @@ TEST_CASE_FIXTURE(Fixture, "indexing_into_a_cyclic_union_doesnt_crash") )"); // this is a cyclic union of number arrays, so it _is_ a table, even if it's a nonsense type. - // no need to generate a NotATable error here. - if (FFlag::LuauAcceptIndexingTableUnionsIntersections) + // no need to generate a NotATable error here. The new solver automatically handles this and + // correctly reports no errors. + if (FFlag::LuauAcceptIndexingTableUnionsIntersections || FFlag::LuauSolverV2) LUAU_REQUIRE_NO_ERRORS(result); else LUAU_REQUIRE_ERROR_COUNT(1, result);