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);
// 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.

View File

@ -96,6 +96,22 @@ struct SubtypingEnvironment
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
* to tentatively map that generic onto a type on the other side.
@ -112,10 +128,6 @@ struct SubtypingEnvironment
DenseHashMap<TypeId, TypeId> substitutions{nullptr};
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

View File

@ -13,7 +13,12 @@
#include <unordered_set>
#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 =
{"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::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<FunctionType>(id); ftv && checkFunctionType(ftv))
{
return TypeCorrectKind::CorrectFunctionResult;

View File

@ -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<TypeFun> 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<TypeId> parameters;
std::vector<TypePackId> 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<TableIndexer> 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<TypeId> genericTypes;
std::vector<TypePackId> 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<std::pair<Name, GenericTypeDefinition>> genericDefinitions = createGenerics(signatureScope, fn->generics);
std::vector<std::pair<Name, GenericTypePackDefinition>> 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<AstTypeReference>())
{
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<TypeFun> 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<TypeId> parameters;
std::vector<TypePackId> 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<AstTypeTable>())
{
TableType::Props props;
std::optional<TableIndexer> 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<AstTypeFunction>())
{
// TODO: Recursion limit.
bool hasGenerics = fn->generics.size > 0 || fn->genericPacks.size > 0;
ScopePtr signatureScope = nullptr;
std::vector<TypeId> genericTypes;
std::vector<TypePackId> 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<std::pair<Name, GenericTypeDefinition>> genericDefinitions = createGenerics(signatureScope, fn->generics);
std::vector<std::pair<Name, GenericTypePackDefinition>> 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<AstTypeTypeof>())
{
// 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<TypeId> 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<TypeId> parts;
for (AstType* part : intersectionAnnotation->types)
{
// TODO: Recursion limit.
parts.push_back(resolveType(scope, part, inTypeArguments));
}

View File

@ -27,10 +27,14 @@
#include <algorithm>
#include <utility>
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
{

View File

@ -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);

View File

@ -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 <algorithm>
LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteNewSolverLimit, false);
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
namespace Luau
{
@ -264,50 +267,86 @@ struct ApplyMappedGenerics : Substitution
NotNull<BuiltinTypes> builtinTypes;
NotNull<TypeArena> arena;
MappedGenerics& mappedGenerics;
MappedGenericPacks& mappedGenericPacks;
SubtypingEnvironment& env;
MappedGenerics& mappedGenerics_DEPRECATED;
MappedGenericPacks& mappedGenericPacks_DEPRECATED;
ApplyMappedGenerics(
NotNull<BuiltinTypes> builtinTypes,
NotNull<TypeArena> 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<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
{
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<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);
}
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(
NotNull<BuiltinTypes> builtinTypes,
NotNull<TypeArena> typeArena,
@ -379,10 +486,23 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy, NotNull<Scope
result.isSubtype = false;
}
SubtypingResult boundsResult = isCovariantWith(env, lowerBound, upperBound, scope);
boundsResult.reasoning.clear();
result.andAlso(boundsResult);
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);
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> 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);
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<TypeId> 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<TypeId> 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<GenericType>(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<GenericType>(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<GenericTypePack>(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;

View File

@ -31,6 +31,7 @@
#include <ostream>
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<AnyType>(indexType) || get<UnknownType>(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<AnyType>(indexType) || get<UnknownType>(indexType))
return {NormalizationResult::True, {tt->indexer->indexResultType}};
}
}

View File

@ -37,6 +37,8 @@ LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyUseGuesserDepth, -1);
LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies, false);
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
namespace Luau
{
@ -669,8 +671,16 @@ TypeFunctionReductionResult<TypeId> lenTypeFunction(
if (normTy->hasTopTable() || get<TableType>(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<TypeId> 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<TypeId> indexFunctionImpl(
TypeId indexerTy = follow(typeParams.at(1));
if (isPending(indexerTy, ctx->solver))
{
return {std::nullopt, false, {indexerTy}, {}};
}
std::shared_ptr<const NormalizedType> indexerNormTy = ctx->normalizer->normalize(indexerTy);

View File

@ -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"(

View File

@ -16,6 +16,7 @@
#include "doctest.h"
#include <algorithm>
#include <limits>
#include <sstream>
#include <string_view>
#include <iostream>
@ -27,6 +28,7 @@ LUAU_FASTFLAG(LuauSolverV2);
LUAU_FASTFLAG(DebugLuauFreezeArena);
LUAU_FASTFLAG(DebugLuauLogSolverToJsonFile)
LUAU_FASTFLAG(LuauDCRMagicFunctionTypeChecker);
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
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)
: 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<int>::max())
, frontend(
&fileResolver,
&configResolver,

View File

@ -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;

View File

@ -939,14 +939,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "index_wait_for_pending_no_crash")
Exp = 0,
MaxExp = 100
}
type Keys = index<typeof(PlayerData), keyof<typeof(PlayerData)>>
-- 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)
)");

View File

@ -1125,7 +1125,11 @@ TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments")
TypeMismatch* tm = get<TypeMismatch>(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...), 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<TypeMismatch>(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...), 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> = (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();

View File

@ -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);

View File

@ -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();

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]));
}
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"(

View File

@ -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();

View File

@ -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);