From f5dabc299868f4a55e3c9089515a2adc56025bbd Mon Sep 17 00:00:00 2001 From: vegorov-rbx <75688451+vegorov-rbx@users.noreply.github.com> Date: Fri, 20 Sep 2024 09:53:26 -0700 Subject: [PATCH] Sync to upstream/release/644 (#1432) In this update we improve overall stability of the new type solver and address some type inference issues with it. If you use the new solver and want to use all new fixes included in this release, you have to reference an additional Luau flag: ```c++ LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease) ``` And set its value to `644`: ```c++ DFInt::LuauTypeSolverRelease.value = 644; // Or a higher value for future updates ``` ## New Solver * Fixed a debug assertion failure in autocomplete (Fixes #1391) * Fixed type function distribution issue which transformed `len<>` and `unm<>` into `not<>` (Fixes #1416) * Placed a limit on the possible normalized table intersection size as a temporary measure to avoid hangs and out-of-memory issues for complex type refinements * Internal recursion limits are now respected in the subtyping operations and in autocomplete, to avoid stack overflow crashes * Fixed false positive errors on assignments to tables whose indexers are unions of strings * Fixed memory corruption crashes in subtyping of generic types containing other generic types in their bounds --- Internal Contributors: Co-authored-by: Aaron Weiss Co-authored-by: Andy Friesen Co-authored-by: Vyacheslav Egorov --- Analysis/include/Luau/ConstraintGenerator.h | 5 + Analysis/include/Luau/Subtyping.h | 20 +- Analysis/src/Autocomplete.cpp | 16 +- Analysis/src/ConstraintGenerator.cpp | 419 +++++++++++--------- Analysis/src/ConstraintSolver.cpp | 10 +- Analysis/src/Normalize.cpp | 13 +- Analysis/src/Subtyping.cpp | 198 +++++++-- Analysis/src/TypeChecker2.cpp | 20 +- Analysis/src/TypeFunction.cpp | 28 +- tests/Autocomplete.test.cpp | 37 ++ tests/Fixture.cpp | 8 +- tests/Fixture.h | 28 ++ tests/TypeFunction.test.cpp | 3 - tests/TypeInfer.generics.test.cpp | 39 +- tests/TypeInfer.operators.test.cpp | 4 +- tests/TypeInfer.refinements.test.cpp | 47 +++ tests/TypeInfer.singletons.test.cpp | 21 + tests/TypeInfer.tables.test.cpp | 39 +- tests/TypeInfer.unionTypes.test.cpp | 14 +- 19 files changed, 706 insertions(+), 263 deletions(-) diff --git a/Analysis/include/Luau/ConstraintGenerator.h b/Analysis/include/Luau/ConstraintGenerator.h index eb6e18eb..e7932a35 100644 --- a/Analysis/include/Luau/ConstraintGenerator.h +++ b/Analysis/include/Luau/ConstraintGenerator.h @@ -321,6 +321,11 @@ private: */ 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 18217a6b..09f46c4d 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 ee865edd..868e31f1 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 face6825..56a6795a 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::resolveReferenceType( + const ScopePtr& scope, + AstType* ty, + AstTypeReference* ref, + bool inTypeArguments, + bool replaceErrorWithFresh +) +{ + TypeId result = nullptr; + + if (FFlag::DebugLuauMagicTypes) + { + 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) + { + 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; + + if (ref->prefix.has_value()) + { + alias = scope->lookupImportedType(ref->prefix->value, ref->name.value); + } + else + { + alias = scope->lookupType(ref->name.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()) + { + result = alias->type; + } + else + { + 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) + { + 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); + } + 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}); + + // 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); + } + + return result; +} + +TypeId ConstraintGenerator::resolveTableType(const ScopePtr& scope, AstType* ty, AstTypeTable* tab, bool inTypeArguments, bool replaceErrorWithFresh) +{ + TableType::Props props; + std::optional indexer; + + for (const AstTableProp& prop : tab->props) + { + TypeId propTy = resolveType(scope, prop.type, inTypeArguments); + + Property& p = props[prop.name.value]; + p.typeLocation = prop.location; + + 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; + } + } + + if (AstTableIndexer* astIndexer = tab->indexer) + { + 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))); + } + + return arena->addType(TableType{props, indexer, scope->level, scope.get(), TableState::Sealed}); +} + +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; + + // 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); + + std::vector> genericDefinitions = createGenerics(signatureScope, fn->generics); + std::vector> genericPackDefinitions = createGenericPacks(signatureScope, fn->genericPacks); + + for (const auto& [name, g] : genericDefinitions) + { + genericTypes.push_back(g.ty); + } + + for (const auto& [name, g] : genericPackDefinitions) + { + 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); + + // 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); + + ftv.argNames.reserve(fn->argNames.size); + for (const auto& el : fn->argNames) + { + if (el) + { + 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; if (auto ref = ty->as()) { - if (FFlag::DebugLuauMagicTypes) - { - 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) - { - 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; - - if (ref->prefix.has_value()) - { - alias = scope->lookupImportedType(ref->prefix->value, ref->name.value); - } - else - { - alias = scope->lookupType(ref->name.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()) - { - result = alias->type; - } - else - { - 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) - { - 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); - } - 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}); - - // 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); - } + result = resolveReferenceType(scope, ty, ref, inTypeArguments, replaceErrorWithFresh); } else if (auto tab = ty->as()) { - TableType::Props props; - std::optional indexer; - - for (const AstTableProp& prop : tab->props) - { - // TODO: Recursion limit. - TypeId propTy = resolveType(scope, prop.type, inTypeArguments); - - Property& p = props[prop.name.value]; - p.typeLocation = prop.location; - - 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; - } - } - - if (AstTableIndexer* astIndexer = tab->indexer) - { - 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))); - } - - result = arena->addType(TableType{props, indexer, scope->level, scope.get(), TableState::Sealed}); + result = resolveTableType(scope, ty, tab, inTypeArguments, replaceErrorWithFresh); } else if (auto fn = ty->as()) { - // TODO: Recursion limit. - bool hasGenerics = fn->generics.size > 0 || fn->genericPacks.size > 0; - ScopePtr signatureScope = nullptr; - - std::vector genericTypes; - std::vector genericTypePacks; - - // 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); - - std::vector> genericDefinitions = createGenerics(signatureScope, fn->generics); - std::vector> genericPackDefinitions = createGenericPacks(signatureScope, fn->genericPacks); - - for (const auto& [name, g] : genericDefinitions) - { - genericTypes.push_back(g.ty); - } - - for (const auto& [name, g] : genericPackDefinitions) - { - 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); - - // 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); - - ftv.argNames.reserve(fn->argNames.size); - for (const auto& el : fn->argNames) - { - if (el) - { - const auto& [name, location] = *el; - ftv.argNames.push_back(FunctionArgument{name.value, location}); - } - else - { - ftv.argNames.push_back(std::nullopt); - } - } - - result = arena->addType(std::move(ftv)); + 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 ae02c60a..f7c4fb5e 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 2db2f40c..c768f02c 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 ee199b66..b13a2327 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.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))}); + } + else + { + const auto& bounds = mappedGenerics_DEPRECATED[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))}); + } } 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 7023fba9..ed66453d 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 9ae57fd1..31154cc2 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 7f020b18..82e5c252 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 0b0e1b7c..8f918f0c 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 f50431f3..9b2db5b8 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 2b7b40a3..d3732d60 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 ffd01f24..a84a0206 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 d8bf9152..18c3410d 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 9c26f165..41797384 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 43b1305e..e2efc8fb 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 9d76e7bd..46c1d1c1 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 6cdec4af..0303f546 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);