mirror of
https://github.com/luau-lang/luau.git
synced 2024-11-15 14:25:44 +08:00
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:
parent
e8a7acb802
commit
f5dabc2998
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
{
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
@ -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}};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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"(
|
||||
|
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
)");
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
@ -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"(
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
Loading…
Reference in New Issue
Block a user