Merge branch 'upstream' into merge

This commit is contained in:
Vyacheslav Egorov 2024-09-20 17:38:03 +03:00
commit 0d6b70b80b
19 changed files with 702 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,36 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_response_perf1" * doctest::timeout(0.
CHECK(ac.entryMap.count("Instance"));
}
TEST_CASE_FIXTURE(ACFixture, "autocomplete_subtyping_recursion_limit")
{
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);