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 <aaronweiss@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
This commit is contained in:
vegorov-rbx 2024-09-20 09:53:26 -07:00 committed by GitHub
parent e8a7acb802
commit f5dabc2998
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 706 additions and 263 deletions

View File

@ -321,6 +321,11 @@ private:
*/ */
void checkFunctionBody(const ScopePtr& scope, AstExprFunction* fn); 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. * Resolves a type from its AST annotation.
* @param scope the scope that the type annotation appears within. * @param scope the scope that the type annotation appears within.

View File

@ -96,6 +96,22 @@ struct SubtypingEnvironment
DenseHashSet<TypeId> upperBound{nullptr}; DenseHashSet<TypeId> 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<TypeId> applyMappedGenerics(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty);
const TypeId* tryFindSubstitution(TypeId ty) const;
const SubtypingResult* tryFindSubtypingResult(std::pair<TypeId, TypeId> 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 * 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. * to tentatively map that generic onto a type on the other side.
@ -112,10 +128,6 @@ struct SubtypingEnvironment
DenseHashMap<TypeId, TypeId> substitutions{nullptr}; DenseHashMap<TypeId, TypeId> substitutions{nullptr};
DenseHashMap<std::pair<TypeId, TypeId>, SubtypingResult, TypePairHash> ephemeralCache{{}}; DenseHashMap<std::pair<TypeId, TypeId>, SubtypingResult, TypePairHash> ephemeralCache{{}};
/// Applies `mappedGenerics` to the given type.
/// This is used specifically to substitute for generics in type function instances.
std::optional<TypeId> applyMappedGenerics(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty);
}; };
struct Subtyping struct Subtyping

View File

@ -13,7 +13,12 @@
#include <unordered_set> #include <unordered_set>
#include <utility> #include <utility>
LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauAutocompleteNewSolverLimit)
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
LUAU_FASTINT(LuauTypeInferIterationLimit)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
static const std::unordered_set<std::string> kStatementStartingKeywords = static const std::unordered_set<std::string> kStatementStartingKeywords =
{"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"}; {"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> scope, T
if (FFlag::LuauSolverV2) 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}}; Subtyping subtyping{builtinTypes, NotNull{typeArena}, NotNull{&normalizer}, NotNull{&iceReporter}};
return subtyping.isSubtype(subTy, superTy, scope).isSubtype; return subtyping.isSubtype(subTy, superTy, scope).isSubtype;
@ -199,6 +210,9 @@ static TypeCorrectKind checkTypeCorrectKind(
{ {
for (TypeId id : itv->parts) for (TypeId id : itv->parts)
{ {
if (DFInt::LuauTypeSolverRelease >= 644)
id = follow(id);
if (const FunctionType* ftv = get<FunctionType>(id); ftv && checkFunctionType(ftv)) if (const FunctionType* ftv = get<FunctionType>(id); ftv && checkFunctionType(ftv))
{ {
return TypeCorrectKind::CorrectFunctionResult; return TypeCorrectKind::CorrectFunctionResult;

View File

@ -2949,12 +2949,16 @@ void ConstraintGenerator::checkFunctionBody(const ScopePtr& scope, AstExprFuncti
addConstraint(scope, fn->location, PackSubtypeConstraint{builtinTypes->emptyTypePack, scope->returnType}); 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; TypeId result = nullptr;
if (auto ref = ty->as<AstTypeReference>())
{
if (FFlag::DebugLuauMagicTypes) if (FFlag::DebugLuauMagicTypes)
{ {
if (ref->name == "_luau_ice") if (ref->name == "_luau_ice")
@ -3037,15 +3041,17 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
if (replaceErrorWithFresh) if (replaceErrorWithFresh)
result = freshType(scope); result = freshType(scope);
} }
return result;
} }
else if (auto tab = ty->as<AstTypeTable>())
TypeId ConstraintGenerator::resolveTableType(const ScopePtr& scope, AstType* ty, AstTypeTable* tab, bool inTypeArguments, bool replaceErrorWithFresh)
{ {
TableType::Props props; TableType::Props props;
std::optional<TableIndexer> indexer; std::optional<TableIndexer> indexer;
for (const AstTableProp& prop : tab->props) for (const AstTableProp& prop : tab->props)
{ {
// TODO: Recursion limit.
TypeId propTy = resolveType(scope, prop.type, inTypeArguments); TypeId propTy = resolveType(scope, prop.type, inTypeArguments);
Property& p = props[prop.name.value]; Property& p = props[prop.name.value];
@ -3079,7 +3085,6 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"write keyword is illegal here"}); reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"write keyword is illegal here"});
else if (astIndexer->access == AstTableAccess::ReadWrite) else if (astIndexer->access == AstTableAccess::ReadWrite)
{ {
// TODO: Recursion limit.
indexer = TableIndexer{ indexer = TableIndexer{
resolveType(scope, astIndexer->indexType, inTypeArguments), resolveType(scope, astIndexer->indexType, inTypeArguments),
resolveType(scope, astIndexer->resultType, inTypeArguments), resolveType(scope, astIndexer->resultType, inTypeArguments),
@ -3089,11 +3094,17 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
ice->ice("Unexpected property access " + std::to_string(int(astIndexer->access))); ice->ice("Unexpected property access " + std::to_string(int(astIndexer->access)));
} }
result = arena->addType(TableType{props, indexer, scope->level, scope.get(), TableState::Sealed}); return arena->addType(TableType{props, indexer, scope->level, scope.get(), TableState::Sealed});
} }
else if (auto fn = ty->as<AstTypeFunction>())
TypeId ConstraintGenerator::resolveFunctionType(
const ScopePtr& scope,
AstType* ty,
AstTypeFunction* fn,
bool inTypeArguments,
bool replaceErrorWithFresh
)
{ {
// TODO: Recursion limit.
bool hasGenerics = fn->generics.size > 0 || fn->genericPacks.size > 0; bool hasGenerics = fn->generics.size > 0 || fn->genericPacks.size > 0;
ScopePtr signatureScope = nullptr; ScopePtr signatureScope = nullptr;
@ -3154,11 +3165,27 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
} }
} }
result = arena->addType(std::move(ftv)); 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<AstTypeReference>())
{
result = resolveReferenceType(scope, ty, ref, inTypeArguments, replaceErrorWithFresh);
}
else if (auto tab = ty->as<AstTypeTable>())
{
result = resolveTableType(scope, ty, tab, inTypeArguments, replaceErrorWithFresh);
}
else if (auto fn = ty->as<AstTypeFunction>())
{
result = resolveFunctionType(scope, ty, fn, inTypeArguments, replaceErrorWithFresh);
} }
else if (auto tof = ty->as<AstTypeTypeof>()) else if (auto tof = ty->as<AstTypeTypeof>())
{ {
// TODO: Recursion limit.
TypeId exprType = check(scope, tof->expr).ty; TypeId exprType = check(scope, tof->expr).ty;
result = exprType; result = exprType;
} }
@ -3167,7 +3194,6 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
std::vector<TypeId> parts; std::vector<TypeId> parts;
for (AstType* part : unionAnnotation->types) for (AstType* part : unionAnnotation->types)
{ {
// TODO: Recursion limit.
parts.push_back(resolveType(scope, part, inTypeArguments)); parts.push_back(resolveType(scope, part, inTypeArguments));
} }
@ -3178,7 +3204,6 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
std::vector<TypeId> parts; std::vector<TypeId> parts;
for (AstType* part : intersectionAnnotation->types) for (AstType* part : intersectionAnnotation->types)
{ {
// TODO: Recursion limit.
parts.push_back(resolveType(scope, part, inTypeArguments)); parts.push_back(resolveType(scope, part, inTypeArguments));
} }

View File

@ -27,10 +27,14 @@
#include <algorithm> #include <algorithm>
#include <utility> #include <utility>
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false); LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false)
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverIncludeDependencies, false) LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverIncludeDependencies, false)
LUAU_FASTFLAGVARIABLE(DebugLuauLogBindings, false); LUAU_FASTFLAGVARIABLE(DebugLuauLogBindings, false)
LUAU_FASTINTVARIABLE(LuauSolverRecursionLimit, 500); 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 namespace Luau
{ {

View File

@ -21,11 +21,12 @@ LUAU_FASTFLAGVARIABLE(LuauNormalizeNotUnknownIntersection, false);
LUAU_FASTFLAGVARIABLE(LuauFixReduceStackPressure, false); LUAU_FASTFLAGVARIABLE(LuauFixReduceStackPressure, false);
LUAU_FASTFLAGVARIABLE(LuauFixCyclicTablesBlowingStack, 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_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000);
LUAU_FASTFLAG(LuauSolverV2); LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAGVARIABLE(LuauUseNormalizeIntersectionLimit, false)
LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200)
static bool fixReduceStackPressure() static bool fixReduceStackPressure()
{ {
return FFlag::LuauFixReduceStackPressure || FFlag::LuauSolverV2; return FFlag::LuauFixReduceStackPressure || FFlag::LuauSolverV2;
@ -3035,6 +3036,14 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor
return unionNormals(here, there, ignoreSmallerTyvars); 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); here.booleans = intersectionOfBools(here.booleans, there.booleans);
intersectClasses(here.classes, there.classes); intersectClasses(here.classes, there.classes);

View File

@ -5,6 +5,7 @@
#include "Luau/Common.h" #include "Luau/Common.h"
#include "Luau/Error.h" #include "Luau/Error.h"
#include "Luau/Normalize.h" #include "Luau/Normalize.h"
#include "Luau/RecursionCounter.h"
#include "Luau/Scope.h" #include "Luau/Scope.h"
#include "Luau/StringUtils.h" #include "Luau/StringUtils.h"
#include "Luau/Substitution.h" #include "Luau/Substitution.h"
@ -21,6 +22,8 @@
#include <algorithm> #include <algorithm>
LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity, false); LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteNewSolverLimit, false);
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
namespace Luau namespace Luau
{ {
@ -264,37 +267,48 @@ struct ApplyMappedGenerics : Substitution
NotNull<BuiltinTypes> builtinTypes; NotNull<BuiltinTypes> builtinTypes;
NotNull<TypeArena> arena; NotNull<TypeArena> arena;
MappedGenerics& mappedGenerics; SubtypingEnvironment& env;
MappedGenericPacks& mappedGenericPacks;
MappedGenerics& mappedGenerics_DEPRECATED;
MappedGenericPacks& mappedGenericPacks_DEPRECATED;
ApplyMappedGenerics( ApplyMappedGenerics(
NotNull<BuiltinTypes> builtinTypes, NotNull<BuiltinTypes> builtinTypes,
NotNull<TypeArena> arena, NotNull<TypeArena> arena,
SubtypingEnvironment& env,
MappedGenerics& mappedGenerics, MappedGenerics& mappedGenerics,
MappedGenericPacks& mappedGenericPacks MappedGenericPacks& mappedGenericPacks
) )
: Substitution(TxnLog::empty(), arena) : Substitution(TxnLog::empty(), arena)
, builtinTypes(builtinTypes) , builtinTypes(builtinTypes)
, arena(arena) , arena(arena)
, mappedGenerics(mappedGenerics) , env(env)
, mappedGenericPacks(mappedGenericPacks) , mappedGenerics_DEPRECATED(mappedGenerics)
, mappedGenericPacks_DEPRECATED(mappedGenericPacks)
{ {
} }
bool isDirty(TypeId ty) override 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 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 TypeId clean(TypeId ty) override
{ {
const auto& bounds = mappedGenerics[ty]; if (DFInt::LuauTypeSolverRelease >= 644)
{
const auto& bounds = env.getMappedTypeBounds(ty);
if (bounds.upperBound.empty()) if (bounds.upperBound.empty())
return builtinTypes->unknownType; return builtinTypes->unknownType;
@ -304,10 +318,35 @@ struct ApplyMappedGenerics : Substitution
return arena->addType(IntersectionType{std::vector<TypeId>(begin(bounds.upperBound), end(bounds.upperBound))}); return arena->addType(IntersectionType{std::vector<TypeId>(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<TypeId>(begin(bounds.upperBound), end(bounds.upperBound))});
}
}
TypePackId clean(TypePackId tp) override 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 bool ignoreChildren(TypeId ty) override
@ -325,10 +364,78 @@ struct ApplyMappedGenerics : Substitution
std::optional<TypeId> SubtypingEnvironment::applyMappedGenerics(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty) std::optional<TypeId> SubtypingEnvironment::applyMappedGenerics(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty)
{ {
ApplyMappedGenerics amg{builtinTypes, arena, mappedGenerics, mappedGenericPacks}; ApplyMappedGenerics amg{builtinTypes, arena, *this, mappedGenerics, mappedGenericPacks};
return amg.substitute(ty); 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<TypeId, TypeId> 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( Subtyping::Subtyping(
NotNull<BuiltinTypes> builtinTypes, NotNull<BuiltinTypes> builtinTypes,
NotNull<TypeArena> typeArena, NotNull<TypeArena> typeArena,
@ -379,11 +486,24 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy, NotNull<Scope
result.isSubtype = false; result.isSubtype = false;
} }
if (DFInt::LuauTypeSolverRelease >= 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); SubtypingResult boundsResult = isCovariantWith(env, lowerBound, upperBound, scope);
boundsResult.reasoning.clear(); boundsResult.reasoning.clear();
result.andAlso(boundsResult); result.andAlso(boundsResult);
} }
}
/* TODO: We presently don't store subtype test results in the persistent /* TODO: We presently don't store subtype test results in the persistent
* cache if the left-side type is a generic function. * cache if the left-side type is a generic function.
@ -442,20 +562,36 @@ struct SeenSetPopper
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy, NotNull<Scope> scope) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy, NotNull<Scope> scope)
{ {
std::optional<RecursionCounter> 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); subTy = follow(subTy);
superTy = follow(superTy); 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; 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; superTy = *superIt;
SubtypingResult* cachedResult = resultCache.find({subTy, superTy}); const SubtypingResult* cachedResult = resultCache.find({subTy, superTy});
if (cachedResult) if (cachedResult)
return *cachedResult; return *cachedResult;
cachedResult = env.ephemeralCache.find({subTy, superTy}); cachedResult = DFInt::LuauTypeSolverRelease >= 644 ? env.tryFindSubtypingResult({subTy, superTy}) : env.ephemeralCache.find({subTy, superTy});
if (cachedResult) if (cachedResult)
return *cachedResult; return *cachedResult;
@ -700,7 +836,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
std::vector<TypeId> headSlice(begin(superHead), begin(superHead) + headSize); std::vector<TypeId> headSlice(begin(superHead), begin(superHead) + headSize);
TypePackId superTailPack = arena->addTypePack(std::move(headSlice), superTail); 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". // TODO: TypePath can't express "slice of a pack + its tail".
results.push_back(isCovariantWith(env, *other, superTailPack, scope).withSubComponent(TypePath::PackField::Tail)); results.push_back(isCovariantWith(env, *other, superTailPack, scope).withSubComponent(TypePath::PackField::Tail));
else else
@ -755,7 +892,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
std::vector<TypeId> headSlice(begin(subHead), begin(subHead) + headSize); std::vector<TypeId> headSlice(begin(subHead), begin(subHead) + headSize);
TypePackId subTailPack = arena->addTypePack(std::move(headSlice), subTail); 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". // TODO: TypePath can't express "slice of a pack + its tail".
results.push_back(isContravariantWith(env, subTailPack, *other, scope).withSuperComponent(TypePath::PackField::Tail)); results.push_back(isContravariantWith(env, subTailPack, *other, scope).withSuperComponent(TypePath::PackField::Tail));
else else
@ -1688,6 +1826,12 @@ bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypeId subTy, TypeId supe
if (!get<GenericType>(subTy)) if (!get<GenericType>(subTy))
return false; 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); env.mappedGenerics[subTy].upperBound.insert(superTy);
} }
else else
@ -1695,6 +1839,12 @@ bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypeId subTy, TypeId supe
if (!get<GenericType>(superTy)) if (!get<GenericType>(superTy))
return false; 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); env.mappedGenerics[superTy].lowerBound.insert(subTy);
} }
@ -1740,7 +1890,7 @@ bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypePackId subTp, TypePac
if (!get<GenericTypePack>(subTp)) if (!get<GenericTypePack>(subTp))
return false; 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; return *m == superTp;
env.mappedGenericPacks[subTp] = superTp; env.mappedGenericPacks[subTp] = superTp;

View File

@ -31,6 +31,7 @@
#include <ostream> #include <ostream>
LUAU_FASTFLAG(DebugLuauMagicTypes) LUAU_FASTFLAG(DebugLuauMagicTypes)
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
namespace Luau namespace Luau
{ {
@ -3012,12 +3013,21 @@ PropertyType TypeChecker2::hasIndexTypeFromType(
if (tt->indexer) if (tt->indexer)
{ {
TypeId indexType = follow(tt->indexer->indexType); TypeId indexType = follow(tt->indexer->indexType);
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)) if (isPrim(indexType, PrimitiveType::String))
return {NormalizationResult::True, {tt->indexer->indexResultType}}; return {NormalizationResult::True, {tt->indexer->indexResultType}};
// If the indexer looks like { [any] : _} - the prop lookup should be allowed! // If the indexer looks like { [any] : _} - the prop lookup should be allowed!
else if (get<AnyType>(indexType) || get<UnknownType>(indexType)) else if (get<AnyType>(indexType) || get<UnknownType>(indexType))
return {NormalizationResult::True, {tt->indexer->indexResultType}}; return {NormalizationResult::True, {tt->indexer->indexResultType}};
} }
}
// if we are in a conditional context, we treat the property as present and `unknown` because // if we are in a conditional context, we treat the property as present and `unknown` because

View File

@ -37,6 +37,8 @@ LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyUseGuesserDepth, -1);
LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies, false); LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies, false);
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
namespace Luau namespace Luau
{ {
@ -669,8 +671,16 @@ TypeFunctionReductionResult<TypeId> lenTypeFunction(
if (normTy->hasTopTable() || get<TableType>(normalizedOperand)) if (normTy->hasTopTable() || get<TableType>(normalizedOperand))
return {ctx->builtins->numberType, false, {}, {}}; return {ctx->builtins->numberType, false, {}, {}};
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)) if (auto result = tryDistributeTypeFunctionApp(notTypeFunction, instance, typeParams, packParams, ctx))
return *result; return *result;
}
// findMetatableEntry demands the ability to emit errors, so we must give it // 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. // the necessary state to do that, even if we intend to just eat the errors.
@ -758,8 +768,16 @@ TypeFunctionReductionResult<TypeId> unmTypeFunction(
if (normTy->isExactlyNumber()) if (normTy->isExactlyNumber())
return {ctx->builtins->numberType, false, {}, {}}; return {ctx->builtins->numberType, false, {}, {}};
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)) if (auto result = tryDistributeTypeFunctionApp(notTypeFunction, instance, typeParams, packParams, ctx))
return *result; return *result;
}
// findMetatableEntry demands the ability to emit errors, so we must give it // 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. // the necessary state to do that, even if we intend to just eat the errors.
@ -2208,9 +2226,7 @@ TypeFunctionReductionResult<TypeId> indexFunctionImpl(
TypeId indexerTy = follow(typeParams.at(1)); TypeId indexerTy = follow(typeParams.at(1));
if (isPending(indexerTy, ctx->solver)) if (isPending(indexerTy, ctx->solver))
{
return {std::nullopt, false, {indexerTy}, {}}; return {std::nullopt, false, {indexerTy}, {}};
}
std::shared_ptr<const NormalizedType> indexerNormTy = ctx->normalizer->normalize(indexerTy); std::shared_ptr<const NormalizedType> indexerNormTy = ctx->normalizer->normalize(indexerTy);

View File

@ -15,6 +15,9 @@
LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2) LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2)
LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel) LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel)
LUAU_FASTFLAG(LuauAutocompleteNewSolverLimit)
LUAU_FASTINT(LuauTypeInferRecursionLimit)
LUAU_FASTFLAG(LuauUseNormalizeIntersectionLimit)
using namespace Luau; using namespace Luau;
@ -3815,6 +3818,40 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_response_perf1" * doctest::timeout(0.
CHECK(ac.entryMap.count("Instance")); 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") TEST_CASE_FIXTURE(ACFixture, "strict_mode_force")
{ {
check(R"( check(R"(

View File

@ -16,6 +16,7 @@
#include "doctest.h" #include "doctest.h"
#include <algorithm> #include <algorithm>
#include <limits>
#include <sstream> #include <sstream>
#include <string_view> #include <string_view>
#include <iostream> #include <iostream>
@ -27,6 +28,7 @@ LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAG(DebugLuauFreezeArena); LUAU_FASTFLAG(DebugLuauFreezeArena);
LUAU_FASTFLAG(DebugLuauLogSolverToJsonFile) LUAU_FASTFLAG(DebugLuauLogSolverToJsonFile)
LUAU_FASTFLAG(LuauDCRMagicFunctionTypeChecker); LUAU_FASTFLAG(LuauDCRMagicFunctionTypeChecker);
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
extern std::optional<unsigned> randomSeed; // tests/main.cpp extern std::optional<unsigned> randomSeed; // tests/main.cpp
@ -152,8 +154,12 @@ const Config& TestConfigResolver::getConfig(const ModuleName& name) const
Fixture::Fixture(bool freeze, bool prepareAutocomplete) Fixture::Fixture(bool freeze, bool prepareAutocomplete)
: sff_DebugLuauFreezeArena(FFlag::DebugLuauFreezeArena, freeze) : 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) , 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<int>::max())
, frontend( , frontend(
&fileResolver, &fileResolver,
&configResolver, &configResolver,

View File

@ -98,9 +98,37 @@ struct Fixture
TypeId requireTypeAlias(const std::string& name); TypeId requireTypeAlias(const std::string& name);
TypeId requireExportedType(const ModuleName& moduleName, 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; 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; 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; TestFileResolver fileResolver;
TestConfigResolver configResolver; TestConfigResolver configResolver;
NullModuleResolver moduleResolver; NullModuleResolver moduleResolver;

View File

@ -939,14 +939,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "index_wait_for_pending_no_crash")
Exp = 0, Exp = 0,
MaxExp = 100 MaxExp = 100
} }
type Keys = index<typeof(PlayerData), keyof<typeof(PlayerData)>> type Keys = index<typeof(PlayerData), keyof<typeof(PlayerData)>>
-- This function makes it think that there's going to be a pending expansion -- This function makes it think that there's going to be a pending expansion
local function UpdateData(key: Keys, value) local function UpdateData(key: Keys, value)
PlayerData[key] = value PlayerData[key] = value
end end
UpdateData("Coins", 2) UpdateData("Coins", 2)
)"); )");

View File

@ -1125,7 +1125,11 @@ TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments")
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]); TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm); REQUIRE(tm);
CHECK_EQ("((number) -> number, string) -> number", toString(tm->wantedType)); 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...), a) -> (b...)", toString(tm->givenType)); CHECK_EQ("<a, b...>((a) -> (b...), a) -> (b...)", toString(tm->givenType));
else else
CHECK_EQ("((number) -> number, number) -> number", toString(tm->givenType)); 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<TypeMismatch>(result.errors[0]); TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
REQUIRE(tm); REQUIRE(tm);
CHECK_EQ("(string, string) -> number", toString(tm->wantedType)); 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...), a) -> (b...)", toString(tm->givenType)); CHECK_EQ("<a, b...>((a) -> (b...), a) -> (b...)", toString(tm->givenType));
else else
CHECK_EQ("((string) -> number, string) -> number", toString(*tm->givenType)); 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); 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> = (A) -> ()
type BasicStateAction<S> = ((S) -> S) | S
function updateReducer<S, I, A>(reducer: (S, A) -> S, initialArg: I, init: ((I) -> S)?): (S, Dispatch<A>)
return 1 :: any
end
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S
return action
end
function updateState<S>(initialState: (() -> S) | S): (S, Dispatch<BasicStateAction<S>>)
return updateReducer(basicStateReducer, initialState)
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View File

@ -1576,7 +1576,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "compare_singleton_string_to_string")
end 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); LUAU_REQUIRE_NO_ERRORS(result);
else else
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);

View File

@ -8,6 +8,7 @@
#include "doctest.h" #include "doctest.h"
LUAU_FASTFLAG(LuauSolverV2) LUAU_FASTFLAG(LuauSolverV2)
LUAU_FASTFLAG(LuauUseNormalizeIntersectionLimit)
using namespace Luau; 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(); TEST_SUITE_END();

View File

@ -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])); 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") TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(

View File

@ -21,6 +21,7 @@ LUAU_FASTFLAG(LuauFixIndexerSubtypingOrdering)
LUAU_FASTFLAG(LuauAcceptIndexingTableUnionsIntersections) LUAU_FASTFLAG(LuauAcceptIndexingTableUnionsIntersections)
LUAU_DYNAMIC_FASTFLAG(LuauImproveNonFunctionCallError) LUAU_DYNAMIC_FASTFLAG(LuauImproveNonFunctionCallError)
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
TEST_SUITE_BEGIN("TableTests"); TEST_SUITE_BEGIN("TableTests");
@ -2653,12 +2654,15 @@ local y = #x
TEST_CASE_FIXTURE(Fixture, "length_operator_union_errors") TEST_CASE_FIXTURE(Fixture, "length_operator_union_errors")
{ {
ScopedFastFlag _{FFlag::LuauSolverV2, true};
CheckResult result = check(R"( CheckResult result = check(R"(
local x: {number} | number | string local x: {number} | number | string
local y = #x 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") 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); LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::LuauSolverV2) 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
{ {
TypeError e{ TypeError e{
Location{{5, 20}, {5, 21}}, Location{{5, 20}, {5, 21}},
CannotCallNonFunction{builtinTypes->numberType}, CannotCallNonFunction{builtinTypes->numberType},
}; };
CHECK(result.errors[0] == e); 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") TEST_CASE_FIXTURE(BuiltinsFixture, "table_call_metamethod_generic")
@ -4832,4 +4836,19 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "indexing_branching_table2")
CHECK("any" == toString(requireType("test2"))); 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(); TEST_SUITE_END();

View File

@ -419,6 +419,9 @@ TEST_CASE_FIXTURE(Fixture, "optional_assignment_errors_2")
TEST_CASE_FIXTURE(Fixture, "optional_length_error") TEST_CASE_FIXTURE(Fixture, "optional_length_error")
{ {
ScopedFastFlag _{FFlag::LuauSolverV2, true};
CheckResult result = check(R"( CheckResult result = check(R"(
type A = {number} type A = {number}
function f(a: A?) function f(a: A?)
@ -426,8 +429,10 @@ TEST_CASE_FIXTURE(Fixture, "optional_length_error")
end end
)"); )");
LUAU_REQUIRE_ERROR_COUNT(1, result); // CLI-119936: This shouldn't double error but does under the new solver.
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0])); 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") 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. // 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. // no need to generate a NotATable error here. The new solver automatically handles this and
if (FFlag::LuauAcceptIndexingTableUnionsIntersections) // correctly reports no errors.
if (FFlag::LuauAcceptIndexingTableUnionsIntersections || FFlag::LuauSolverV2)
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
else else
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);