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);
|
void checkFunctionBody(const ScopePtr& scope, AstExprFunction* fn);
|
||||||
|
|
||||||
|
// Specializations of 'resolveType' below
|
||||||
|
TypeId resolveReferenceType(const ScopePtr& scope, AstType* ty, AstTypeReference* ref, bool inTypeArguments, bool replaceErrorWithFresh);
|
||||||
|
TypeId resolveTableType(const ScopePtr& scope, AstType* ty, AstTypeTable* tab, bool inTypeArguments, bool replaceErrorWithFresh);
|
||||||
|
TypeId resolveFunctionType(const ScopePtr& scope, AstType* ty, AstTypeFunction* fn, bool inTypeArguments, bool replaceErrorWithFresh);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves a type from its AST annotation.
|
* Resolves a type from its AST annotation.
|
||||||
* @param scope the scope that the type annotation appears within.
|
* @param scope the scope that the type annotation appears within.
|
||||||
|
@ -96,6 +96,22 @@ struct SubtypingEnvironment
|
|||||||
DenseHashSet<TypeId> upperBound{nullptr};
|
DenseHashSet<TypeId> upperBound{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* For nested subtyping relationship tests of mapped generic bounds, we keep the outer environment immutable */
|
||||||
|
SubtypingEnvironment* parent = nullptr;
|
||||||
|
|
||||||
|
/// Applies `mappedGenerics` to the given type.
|
||||||
|
/// This is used specifically to substitute for generics in type function instances.
|
||||||
|
std::optional<TypeId> applyMappedGenerics(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty);
|
||||||
|
|
||||||
|
const TypeId* tryFindSubstitution(TypeId ty) const;
|
||||||
|
const SubtypingResult* tryFindSubtypingResult(std::pair<TypeId, TypeId> subAndSuper) const;
|
||||||
|
|
||||||
|
bool containsMappedType(TypeId ty) const;
|
||||||
|
bool containsMappedPack(TypePackId tp) const;
|
||||||
|
|
||||||
|
GenericBounds& getMappedTypeBounds(TypeId ty);
|
||||||
|
TypePackId* getMappedPackBounds(TypePackId tp);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* When we encounter a generic over the course of a subtyping test, we need
|
* When we encounter a generic over the course of a subtyping test, we need
|
||||||
* to tentatively map that generic onto a type on the other side.
|
* to tentatively map that generic onto a type on the other side.
|
||||||
@ -112,10 +128,6 @@ struct SubtypingEnvironment
|
|||||||
DenseHashMap<TypeId, TypeId> substitutions{nullptr};
|
DenseHashMap<TypeId, TypeId> substitutions{nullptr};
|
||||||
|
|
||||||
DenseHashMap<std::pair<TypeId, TypeId>, SubtypingResult, TypePairHash> ephemeralCache{{}};
|
DenseHashMap<std::pair<TypeId, TypeId>, SubtypingResult, TypePairHash> ephemeralCache{{}};
|
||||||
|
|
||||||
/// Applies `mappedGenerics` to the given type.
|
|
||||||
/// This is used specifically to substitute for generics in type function instances.
|
|
||||||
std::optional<TypeId> applyMappedGenerics(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Subtyping
|
struct Subtyping
|
||||||
|
@ -13,7 +13,12 @@
|
|||||||
#include <unordered_set>
|
#include <unordered_set>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
LUAU_FASTFLAG(LuauSolverV2);
|
LUAU_FASTFLAG(LuauSolverV2)
|
||||||
|
LUAU_FASTFLAG(LuauAutocompleteNewSolverLimit)
|
||||||
|
|
||||||
|
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
|
||||||
|
LUAU_FASTINT(LuauTypeInferIterationLimit)
|
||||||
|
LUAU_FASTINT(LuauTypeInferRecursionLimit)
|
||||||
|
|
||||||
static const std::unordered_set<std::string> kStatementStartingKeywords =
|
static const std::unordered_set<std::string> kStatementStartingKeywords =
|
||||||
{"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"};
|
{"while", "if", "local", "repeat", "function", "do", "for", "return", "break", "continue", "type", "export"};
|
||||||
@ -144,6 +149,12 @@ static bool checkTypeMatch(TypeId subTy, TypeId superTy, NotNull<Scope> scope, T
|
|||||||
|
|
||||||
if (FFlag::LuauSolverV2)
|
if (FFlag::LuauSolverV2)
|
||||||
{
|
{
|
||||||
|
if (FFlag::LuauAutocompleteNewSolverLimit)
|
||||||
|
{
|
||||||
|
unifierState.counters.recursionLimit = FInt::LuauTypeInferRecursionLimit;
|
||||||
|
unifierState.counters.iterationLimit = FInt::LuauTypeInferIterationLimit;
|
||||||
|
}
|
||||||
|
|
||||||
Subtyping subtyping{builtinTypes, NotNull{typeArena}, NotNull{&normalizer}, NotNull{&iceReporter}};
|
Subtyping subtyping{builtinTypes, NotNull{typeArena}, NotNull{&normalizer}, NotNull{&iceReporter}};
|
||||||
|
|
||||||
return subtyping.isSubtype(subTy, superTy, scope).isSubtype;
|
return subtyping.isSubtype(subTy, superTy, scope).isSubtype;
|
||||||
@ -199,6 +210,9 @@ static TypeCorrectKind checkTypeCorrectKind(
|
|||||||
{
|
{
|
||||||
for (TypeId id : itv->parts)
|
for (TypeId id : itv->parts)
|
||||||
{
|
{
|
||||||
|
if (DFInt::LuauTypeSolverRelease >= 644)
|
||||||
|
id = follow(id);
|
||||||
|
|
||||||
if (const FunctionType* ftv = get<FunctionType>(id); ftv && checkFunctionType(ftv))
|
if (const FunctionType* ftv = get<FunctionType>(id); ftv && checkFunctionType(ftv))
|
||||||
{
|
{
|
||||||
return TypeCorrectKind::CorrectFunctionResult;
|
return TypeCorrectKind::CorrectFunctionResult;
|
||||||
|
@ -2949,12 +2949,16 @@ void ConstraintGenerator::checkFunctionBody(const ScopePtr& scope, AstExprFuncti
|
|||||||
addConstraint(scope, fn->location, PackSubtypeConstraint{builtinTypes->emptyTypePack, scope->returnType});
|
addConstraint(scope, fn->location, PackSubtypeConstraint{builtinTypes->emptyTypePack, scope->returnType});
|
||||||
}
|
}
|
||||||
|
|
||||||
TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh)
|
TypeId ConstraintGenerator::resolveReferenceType(
|
||||||
|
const ScopePtr& scope,
|
||||||
|
AstType* ty,
|
||||||
|
AstTypeReference* ref,
|
||||||
|
bool inTypeArguments,
|
||||||
|
bool replaceErrorWithFresh
|
||||||
|
)
|
||||||
{
|
{
|
||||||
TypeId result = nullptr;
|
TypeId result = nullptr;
|
||||||
|
|
||||||
if (auto ref = ty->as<AstTypeReference>())
|
|
||||||
{
|
|
||||||
if (FFlag::DebugLuauMagicTypes)
|
if (FFlag::DebugLuauMagicTypes)
|
||||||
{
|
{
|
||||||
if (ref->name == "_luau_ice")
|
if (ref->name == "_luau_ice")
|
||||||
@ -3037,15 +3041,17 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
|
|||||||
if (replaceErrorWithFresh)
|
if (replaceErrorWithFresh)
|
||||||
result = freshType(scope);
|
result = freshType(scope);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
else if (auto tab = ty->as<AstTypeTable>())
|
return result;
|
||||||
{
|
}
|
||||||
|
|
||||||
|
TypeId ConstraintGenerator::resolveTableType(const ScopePtr& scope, AstType* ty, AstTypeTable* tab, bool inTypeArguments, bool replaceErrorWithFresh)
|
||||||
|
{
|
||||||
TableType::Props props;
|
TableType::Props props;
|
||||||
std::optional<TableIndexer> indexer;
|
std::optional<TableIndexer> indexer;
|
||||||
|
|
||||||
for (const AstTableProp& prop : tab->props)
|
for (const AstTableProp& prop : tab->props)
|
||||||
{
|
{
|
||||||
// TODO: Recursion limit.
|
|
||||||
TypeId propTy = resolveType(scope, prop.type, inTypeArguments);
|
TypeId propTy = resolveType(scope, prop.type, inTypeArguments);
|
||||||
|
|
||||||
Property& p = props[prop.name.value];
|
Property& p = props[prop.name.value];
|
||||||
@ -3079,7 +3085,6 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
|
|||||||
reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"write keyword is illegal here"});
|
reportError(astIndexer->accessLocation.value_or(Location{}), GenericError{"write keyword is illegal here"});
|
||||||
else if (astIndexer->access == AstTableAccess::ReadWrite)
|
else if (astIndexer->access == AstTableAccess::ReadWrite)
|
||||||
{
|
{
|
||||||
// TODO: Recursion limit.
|
|
||||||
indexer = TableIndexer{
|
indexer = TableIndexer{
|
||||||
resolveType(scope, astIndexer->indexType, inTypeArguments),
|
resolveType(scope, astIndexer->indexType, inTypeArguments),
|
||||||
resolveType(scope, astIndexer->resultType, inTypeArguments),
|
resolveType(scope, astIndexer->resultType, inTypeArguments),
|
||||||
@ -3089,11 +3094,17 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
|
|||||||
ice->ice("Unexpected property access " + std::to_string(int(astIndexer->access)));
|
ice->ice("Unexpected property access " + std::to_string(int(astIndexer->access)));
|
||||||
}
|
}
|
||||||
|
|
||||||
result = arena->addType(TableType{props, indexer, scope->level, scope.get(), TableState::Sealed});
|
return arena->addType(TableType{props, indexer, scope->level, scope.get(), TableState::Sealed});
|
||||||
}
|
}
|
||||||
else if (auto fn = ty->as<AstTypeFunction>())
|
|
||||||
{
|
TypeId ConstraintGenerator::resolveFunctionType(
|
||||||
// TODO: Recursion limit.
|
const ScopePtr& scope,
|
||||||
|
AstType* ty,
|
||||||
|
AstTypeFunction* fn,
|
||||||
|
bool inTypeArguments,
|
||||||
|
bool replaceErrorWithFresh
|
||||||
|
)
|
||||||
|
{
|
||||||
bool hasGenerics = fn->generics.size > 0 || fn->genericPacks.size > 0;
|
bool hasGenerics = fn->generics.size > 0 || fn->genericPacks.size > 0;
|
||||||
ScopePtr signatureScope = nullptr;
|
ScopePtr signatureScope = nullptr;
|
||||||
|
|
||||||
@ -3154,11 +3165,27 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
result = arena->addType(std::move(ftv));
|
return arena->addType(std::move(ftv));
|
||||||
|
}
|
||||||
|
|
||||||
|
TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool inTypeArguments, bool replaceErrorWithFresh)
|
||||||
|
{
|
||||||
|
TypeId result = nullptr;
|
||||||
|
|
||||||
|
if (auto ref = ty->as<AstTypeReference>())
|
||||||
|
{
|
||||||
|
result = resolveReferenceType(scope, ty, ref, inTypeArguments, replaceErrorWithFresh);
|
||||||
|
}
|
||||||
|
else if (auto tab = ty->as<AstTypeTable>())
|
||||||
|
{
|
||||||
|
result = resolveTableType(scope, ty, tab, inTypeArguments, replaceErrorWithFresh);
|
||||||
|
}
|
||||||
|
else if (auto fn = ty->as<AstTypeFunction>())
|
||||||
|
{
|
||||||
|
result = resolveFunctionType(scope, ty, fn, inTypeArguments, replaceErrorWithFresh);
|
||||||
}
|
}
|
||||||
else if (auto tof = ty->as<AstTypeTypeof>())
|
else if (auto tof = ty->as<AstTypeTypeof>())
|
||||||
{
|
{
|
||||||
// TODO: Recursion limit.
|
|
||||||
TypeId exprType = check(scope, tof->expr).ty;
|
TypeId exprType = check(scope, tof->expr).ty;
|
||||||
result = exprType;
|
result = exprType;
|
||||||
}
|
}
|
||||||
@ -3167,7 +3194,6 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
|
|||||||
std::vector<TypeId> parts;
|
std::vector<TypeId> parts;
|
||||||
for (AstType* part : unionAnnotation->types)
|
for (AstType* part : unionAnnotation->types)
|
||||||
{
|
{
|
||||||
// TODO: Recursion limit.
|
|
||||||
parts.push_back(resolveType(scope, part, inTypeArguments));
|
parts.push_back(resolveType(scope, part, inTypeArguments));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3178,7 +3204,6 @@ TypeId ConstraintGenerator::resolveType(const ScopePtr& scope, AstType* ty, bool
|
|||||||
std::vector<TypeId> parts;
|
std::vector<TypeId> parts;
|
||||||
for (AstType* part : intersectionAnnotation->types)
|
for (AstType* part : intersectionAnnotation->types)
|
||||||
{
|
{
|
||||||
// TODO: Recursion limit.
|
|
||||||
parts.push_back(resolveType(scope, part, inTypeArguments));
|
parts.push_back(resolveType(scope, part, inTypeArguments));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,10 +27,14 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false);
|
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolver, false)
|
||||||
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverIncludeDependencies, false)
|
LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverIncludeDependencies, false)
|
||||||
LUAU_FASTFLAGVARIABLE(DebugLuauLogBindings, false);
|
LUAU_FASTFLAGVARIABLE(DebugLuauLogBindings, false)
|
||||||
LUAU_FASTINTVARIABLE(LuauSolverRecursionLimit, 500);
|
LUAU_FASTINTVARIABLE(LuauSolverRecursionLimit, 500)
|
||||||
|
|
||||||
|
// The default value here is 643 because the first release in which this was implemented is 644,
|
||||||
|
// and actively we want new changes to be off by default until they're enabled consciously.
|
||||||
|
LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeSolverRelease, 643)
|
||||||
|
|
||||||
namespace Luau
|
namespace Luau
|
||||||
{
|
{
|
||||||
|
@ -21,11 +21,12 @@ LUAU_FASTFLAGVARIABLE(LuauNormalizeNotUnknownIntersection, false);
|
|||||||
LUAU_FASTFLAGVARIABLE(LuauFixReduceStackPressure, false);
|
LUAU_FASTFLAGVARIABLE(LuauFixReduceStackPressure, false);
|
||||||
LUAU_FASTFLAGVARIABLE(LuauFixCyclicTablesBlowingStack, false);
|
LUAU_FASTFLAGVARIABLE(LuauFixCyclicTablesBlowingStack, false);
|
||||||
|
|
||||||
// This could theoretically be 2000 on amd64, but x86 requires this.
|
|
||||||
LUAU_FASTINTVARIABLE(LuauNormalizeIterationLimit, 1200);
|
|
||||||
LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000);
|
LUAU_FASTINTVARIABLE(LuauNormalizeCacheLimit, 100000);
|
||||||
LUAU_FASTFLAG(LuauSolverV2);
|
LUAU_FASTFLAG(LuauSolverV2);
|
||||||
|
|
||||||
|
LUAU_FASTFLAGVARIABLE(LuauUseNormalizeIntersectionLimit, false)
|
||||||
|
LUAU_FASTINTVARIABLE(LuauNormalizeIntersectionLimit, 200)
|
||||||
|
|
||||||
static bool fixReduceStackPressure()
|
static bool fixReduceStackPressure()
|
||||||
{
|
{
|
||||||
return FFlag::LuauFixReduceStackPressure || FFlag::LuauSolverV2;
|
return FFlag::LuauFixReduceStackPressure || FFlag::LuauSolverV2;
|
||||||
@ -3035,6 +3036,14 @@ NormalizationResult Normalizer::intersectNormals(NormalizedType& here, const Nor
|
|||||||
return unionNormals(here, there, ignoreSmallerTyvars);
|
return unionNormals(here, there, ignoreSmallerTyvars);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (FFlag::LuauUseNormalizeIntersectionLimit)
|
||||||
|
{
|
||||||
|
// Limit based on worst-case expansion of the table intersection
|
||||||
|
// This restriction can be relaxed when table intersection simplification is improved
|
||||||
|
if (here.tables.size() * there.tables.size() >= size_t(FInt::LuauNormalizeIntersectionLimit))
|
||||||
|
return NormalizationResult::HitLimits;
|
||||||
|
}
|
||||||
|
|
||||||
here.booleans = intersectionOfBools(here.booleans, there.booleans);
|
here.booleans = intersectionOfBools(here.booleans, there.booleans);
|
||||||
|
|
||||||
intersectClasses(here.classes, there.classes);
|
intersectClasses(here.classes, there.classes);
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
#include "Luau/Common.h"
|
#include "Luau/Common.h"
|
||||||
#include "Luau/Error.h"
|
#include "Luau/Error.h"
|
||||||
#include "Luau/Normalize.h"
|
#include "Luau/Normalize.h"
|
||||||
|
#include "Luau/RecursionCounter.h"
|
||||||
#include "Luau/Scope.h"
|
#include "Luau/Scope.h"
|
||||||
#include "Luau/StringUtils.h"
|
#include "Luau/StringUtils.h"
|
||||||
#include "Luau/Substitution.h"
|
#include "Luau/Substitution.h"
|
||||||
@ -21,6 +22,8 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
|
||||||
LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity, false);
|
LUAU_FASTFLAGVARIABLE(DebugLuauSubtypingCheckPathValidity, false);
|
||||||
|
LUAU_FASTFLAGVARIABLE(LuauAutocompleteNewSolverLimit, false);
|
||||||
|
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
|
||||||
|
|
||||||
namespace Luau
|
namespace Luau
|
||||||
{
|
{
|
||||||
@ -264,37 +267,48 @@ struct ApplyMappedGenerics : Substitution
|
|||||||
NotNull<BuiltinTypes> builtinTypes;
|
NotNull<BuiltinTypes> builtinTypes;
|
||||||
NotNull<TypeArena> arena;
|
NotNull<TypeArena> arena;
|
||||||
|
|
||||||
MappedGenerics& mappedGenerics;
|
SubtypingEnvironment& env;
|
||||||
MappedGenericPacks& mappedGenericPacks;
|
|
||||||
|
|
||||||
|
MappedGenerics& mappedGenerics_DEPRECATED;
|
||||||
|
MappedGenericPacks& mappedGenericPacks_DEPRECATED;
|
||||||
|
|
||||||
ApplyMappedGenerics(
|
ApplyMappedGenerics(
|
||||||
NotNull<BuiltinTypes> builtinTypes,
|
NotNull<BuiltinTypes> builtinTypes,
|
||||||
NotNull<TypeArena> arena,
|
NotNull<TypeArena> arena,
|
||||||
|
SubtypingEnvironment& env,
|
||||||
MappedGenerics& mappedGenerics,
|
MappedGenerics& mappedGenerics,
|
||||||
MappedGenericPacks& mappedGenericPacks
|
MappedGenericPacks& mappedGenericPacks
|
||||||
)
|
)
|
||||||
: Substitution(TxnLog::empty(), arena)
|
: Substitution(TxnLog::empty(), arena)
|
||||||
, builtinTypes(builtinTypes)
|
, builtinTypes(builtinTypes)
|
||||||
, arena(arena)
|
, arena(arena)
|
||||||
, mappedGenerics(mappedGenerics)
|
, env(env)
|
||||||
, mappedGenericPacks(mappedGenericPacks)
|
, mappedGenerics_DEPRECATED(mappedGenerics)
|
||||||
|
, mappedGenericPacks_DEPRECATED(mappedGenericPacks)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isDirty(TypeId ty) override
|
bool isDirty(TypeId ty) override
|
||||||
{
|
{
|
||||||
return mappedGenerics.contains(ty);
|
if (DFInt::LuauTypeSolverRelease >= 644)
|
||||||
|
return env.containsMappedType(ty);
|
||||||
|
else
|
||||||
|
return mappedGenerics_DEPRECATED.contains(ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isDirty(TypePackId tp) override
|
bool isDirty(TypePackId tp) override
|
||||||
{
|
{
|
||||||
return mappedGenericPacks.contains(tp);
|
if (DFInt::LuauTypeSolverRelease >= 644)
|
||||||
|
return env.containsMappedPack(tp);
|
||||||
|
else
|
||||||
|
return mappedGenericPacks_DEPRECATED.contains(tp);
|
||||||
}
|
}
|
||||||
|
|
||||||
TypeId clean(TypeId ty) override
|
TypeId clean(TypeId ty) override
|
||||||
{
|
{
|
||||||
const auto& bounds = mappedGenerics[ty];
|
if (DFInt::LuauTypeSolverRelease >= 644)
|
||||||
|
{
|
||||||
|
const auto& bounds = env.getMappedTypeBounds(ty);
|
||||||
|
|
||||||
if (bounds.upperBound.empty())
|
if (bounds.upperBound.empty())
|
||||||
return builtinTypes->unknownType;
|
return builtinTypes->unknownType;
|
||||||
@ -304,10 +318,35 @@ struct ApplyMappedGenerics : Substitution
|
|||||||
|
|
||||||
return arena->addType(IntersectionType{std::vector<TypeId>(begin(bounds.upperBound), end(bounds.upperBound))});
|
return arena->addType(IntersectionType{std::vector<TypeId>(begin(bounds.upperBound), end(bounds.upperBound))});
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const auto& bounds = mappedGenerics_DEPRECATED[ty];
|
||||||
|
|
||||||
|
if (bounds.upperBound.empty())
|
||||||
|
return builtinTypes->unknownType;
|
||||||
|
|
||||||
|
if (bounds.upperBound.size() == 1)
|
||||||
|
return *begin(bounds.upperBound);
|
||||||
|
|
||||||
|
return arena->addType(IntersectionType{std::vector<TypeId>(begin(bounds.upperBound), end(bounds.upperBound))});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
TypePackId clean(TypePackId tp) override
|
TypePackId clean(TypePackId tp) override
|
||||||
{
|
{
|
||||||
return mappedGenericPacks[tp];
|
if (DFInt::LuauTypeSolverRelease >= 644)
|
||||||
|
{
|
||||||
|
if (auto it = env.getMappedPackBounds(tp))
|
||||||
|
return *it;
|
||||||
|
|
||||||
|
// Clean is only called when isDirty found a pack bound
|
||||||
|
LUAU_ASSERT(!"Unreachable");
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return mappedGenericPacks_DEPRECATED[tp];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ignoreChildren(TypeId ty) override
|
bool ignoreChildren(TypeId ty) override
|
||||||
@ -325,10 +364,78 @@ struct ApplyMappedGenerics : Substitution
|
|||||||
|
|
||||||
std::optional<TypeId> SubtypingEnvironment::applyMappedGenerics(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty)
|
std::optional<TypeId> SubtypingEnvironment::applyMappedGenerics(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId ty)
|
||||||
{
|
{
|
||||||
ApplyMappedGenerics amg{builtinTypes, arena, mappedGenerics, mappedGenericPacks};
|
ApplyMappedGenerics amg{builtinTypes, arena, *this, mappedGenerics, mappedGenericPacks};
|
||||||
return amg.substitute(ty);
|
return amg.substitute(ty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TypeId* SubtypingEnvironment::tryFindSubstitution(TypeId ty) const
|
||||||
|
{
|
||||||
|
if (auto it = substitutions.find(ty))
|
||||||
|
return it;
|
||||||
|
|
||||||
|
if (parent)
|
||||||
|
return parent->tryFindSubstitution(ty);
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SubtypingResult* SubtypingEnvironment::tryFindSubtypingResult(std::pair<TypeId, TypeId> subAndSuper) const
|
||||||
|
{
|
||||||
|
if (auto it = ephemeralCache.find(subAndSuper))
|
||||||
|
return it;
|
||||||
|
|
||||||
|
if (parent)
|
||||||
|
return parent->tryFindSubtypingResult(subAndSuper);
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SubtypingEnvironment::containsMappedType(TypeId ty) const
|
||||||
|
{
|
||||||
|
if (mappedGenerics.contains(ty))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (parent)
|
||||||
|
return parent->containsMappedType(ty);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool SubtypingEnvironment::containsMappedPack(TypePackId tp) const
|
||||||
|
{
|
||||||
|
if (mappedGenericPacks.contains(tp))
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (parent)
|
||||||
|
return parent->containsMappedPack(tp);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SubtypingEnvironment::GenericBounds& SubtypingEnvironment::getMappedTypeBounds(TypeId ty)
|
||||||
|
{
|
||||||
|
if (auto it = mappedGenerics.find(ty))
|
||||||
|
return *it;
|
||||||
|
|
||||||
|
if (parent)
|
||||||
|
return parent->getMappedTypeBounds(ty);
|
||||||
|
|
||||||
|
LUAU_ASSERT(!"Use containsMappedType before asking for bounds!");
|
||||||
|
return mappedGenerics[ty];
|
||||||
|
}
|
||||||
|
|
||||||
|
TypePackId* SubtypingEnvironment::getMappedPackBounds(TypePackId tp)
|
||||||
|
{
|
||||||
|
if (auto it = mappedGenericPacks.find(tp))
|
||||||
|
return it;
|
||||||
|
|
||||||
|
if (parent)
|
||||||
|
return parent->getMappedPackBounds(tp);
|
||||||
|
|
||||||
|
// This fallback is reachable in valid cases, unlike the final part of getMappedTypeBounds
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
Subtyping::Subtyping(
|
Subtyping::Subtyping(
|
||||||
NotNull<BuiltinTypes> builtinTypes,
|
NotNull<BuiltinTypes> builtinTypes,
|
||||||
NotNull<TypeArena> typeArena,
|
NotNull<TypeArena> typeArena,
|
||||||
@ -379,11 +486,24 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy, NotNull<Scope
|
|||||||
result.isSubtype = false;
|
result.isSubtype = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (DFInt::LuauTypeSolverRelease >= 644)
|
||||||
|
{
|
||||||
|
SubtypingEnvironment boundsEnv;
|
||||||
|
boundsEnv.parent = &env;
|
||||||
|
SubtypingResult boundsResult = isCovariantWith(boundsEnv, lowerBound, upperBound, scope);
|
||||||
|
boundsResult.reasoning.clear();
|
||||||
|
|
||||||
|
result.andAlso(boundsResult);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
SubtypingResult boundsResult = isCovariantWith(env, lowerBound, upperBound, scope);
|
SubtypingResult boundsResult = isCovariantWith(env, lowerBound, upperBound, scope);
|
||||||
boundsResult.reasoning.clear();
|
boundsResult.reasoning.clear();
|
||||||
|
|
||||||
result.andAlso(boundsResult);
|
result.andAlso(boundsResult);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* TODO: We presently don't store subtype test results in the persistent
|
/* TODO: We presently don't store subtype test results in the persistent
|
||||||
* cache if the left-side type is a generic function.
|
* cache if the left-side type is a generic function.
|
||||||
@ -442,20 +562,36 @@ struct SeenSetPopper
|
|||||||
|
|
||||||
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy, NotNull<Scope> scope)
|
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy, NotNull<Scope> scope)
|
||||||
{
|
{
|
||||||
|
std::optional<RecursionCounter> rc;
|
||||||
|
|
||||||
|
if (FFlag::LuauAutocompleteNewSolverLimit)
|
||||||
|
{
|
||||||
|
UnifierCounters& counters = normalizer->sharedState->counters;
|
||||||
|
rc.emplace(&counters.recursionCount);
|
||||||
|
|
||||||
|
if (counters.recursionLimit > 0 && counters.recursionLimit < counters.recursionCount)
|
||||||
|
{
|
||||||
|
SubtypingResult result;
|
||||||
|
result.normalizationTooComplex = true;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
subTy = follow(subTy);
|
subTy = follow(subTy);
|
||||||
superTy = follow(superTy);
|
superTy = follow(superTy);
|
||||||
|
|
||||||
if (TypeId* subIt = env.substitutions.find(subTy); subIt && *subIt)
|
if (const TypeId* subIt = (DFInt::LuauTypeSolverRelease >= 644 ? env.tryFindSubstitution(subTy) : env.substitutions.find(subTy)); subIt && *subIt)
|
||||||
subTy = *subIt;
|
subTy = *subIt;
|
||||||
|
|
||||||
if (TypeId* superIt = env.substitutions.find(superTy); superIt && *superIt)
|
if (const TypeId* superIt = (DFInt::LuauTypeSolverRelease >= 644 ? env.tryFindSubstitution(superTy) : env.substitutions.find(superTy));
|
||||||
|
superIt && *superIt)
|
||||||
superTy = *superIt;
|
superTy = *superIt;
|
||||||
|
|
||||||
SubtypingResult* cachedResult = resultCache.find({subTy, superTy});
|
const SubtypingResult* cachedResult = resultCache.find({subTy, superTy});
|
||||||
if (cachedResult)
|
if (cachedResult)
|
||||||
return *cachedResult;
|
return *cachedResult;
|
||||||
|
|
||||||
cachedResult = env.ephemeralCache.find({subTy, superTy});
|
cachedResult = DFInt::LuauTypeSolverRelease >= 644 ? env.tryFindSubtypingResult({subTy, superTy}) : env.ephemeralCache.find({subTy, superTy});
|
||||||
if (cachedResult)
|
if (cachedResult)
|
||||||
return *cachedResult;
|
return *cachedResult;
|
||||||
|
|
||||||
@ -700,7 +836,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
|
|||||||
std::vector<TypeId> headSlice(begin(superHead), begin(superHead) + headSize);
|
std::vector<TypeId> headSlice(begin(superHead), begin(superHead) + headSize);
|
||||||
TypePackId superTailPack = arena->addTypePack(std::move(headSlice), superTail);
|
TypePackId superTailPack = arena->addTypePack(std::move(headSlice), superTail);
|
||||||
|
|
||||||
if (TypePackId* other = env.mappedGenericPacks.find(*subTail))
|
if (TypePackId* other =
|
||||||
|
(DFInt::LuauTypeSolverRelease >= 644 ? env.getMappedPackBounds(*subTail) : env.mappedGenericPacks.find(*subTail)))
|
||||||
// TODO: TypePath can't express "slice of a pack + its tail".
|
// TODO: TypePath can't express "slice of a pack + its tail".
|
||||||
results.push_back(isCovariantWith(env, *other, superTailPack, scope).withSubComponent(TypePath::PackField::Tail));
|
results.push_back(isCovariantWith(env, *other, superTailPack, scope).withSubComponent(TypePath::PackField::Tail));
|
||||||
else
|
else
|
||||||
@ -755,7 +892,8 @@ SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId
|
|||||||
std::vector<TypeId> headSlice(begin(subHead), begin(subHead) + headSize);
|
std::vector<TypeId> headSlice(begin(subHead), begin(subHead) + headSize);
|
||||||
TypePackId subTailPack = arena->addTypePack(std::move(headSlice), subTail);
|
TypePackId subTailPack = arena->addTypePack(std::move(headSlice), subTail);
|
||||||
|
|
||||||
if (TypePackId* other = env.mappedGenericPacks.find(*superTail))
|
if (TypePackId* other =
|
||||||
|
(DFInt::LuauTypeSolverRelease >= 644 ? env.getMappedPackBounds(*superTail) : env.mappedGenericPacks.find(*superTail)))
|
||||||
// TODO: TypePath can't express "slice of a pack + its tail".
|
// TODO: TypePath can't express "slice of a pack + its tail".
|
||||||
results.push_back(isContravariantWith(env, subTailPack, *other, scope).withSuperComponent(TypePath::PackField::Tail));
|
results.push_back(isContravariantWith(env, subTailPack, *other, scope).withSuperComponent(TypePath::PackField::Tail));
|
||||||
else
|
else
|
||||||
@ -1688,6 +1826,12 @@ bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypeId subTy, TypeId supe
|
|||||||
if (!get<GenericType>(subTy))
|
if (!get<GenericType>(subTy))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (DFInt::LuauTypeSolverRelease >= 644)
|
||||||
|
{
|
||||||
|
if (!env.mappedGenerics.find(subTy) && env.containsMappedType(subTy))
|
||||||
|
iceReporter->ice("attempting to modify bounds of a potentially visited generic");
|
||||||
|
}
|
||||||
|
|
||||||
env.mappedGenerics[subTy].upperBound.insert(superTy);
|
env.mappedGenerics[subTy].upperBound.insert(superTy);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@ -1695,6 +1839,12 @@ bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypeId subTy, TypeId supe
|
|||||||
if (!get<GenericType>(superTy))
|
if (!get<GenericType>(superTy))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
if (DFInt::LuauTypeSolverRelease >= 644)
|
||||||
|
{
|
||||||
|
if (!env.mappedGenerics.find(superTy) && env.containsMappedType(superTy))
|
||||||
|
iceReporter->ice("attempting to modify bounds of a potentially visited generic");
|
||||||
|
}
|
||||||
|
|
||||||
env.mappedGenerics[superTy].lowerBound.insert(subTy);
|
env.mappedGenerics[superTy].lowerBound.insert(subTy);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1740,7 +1890,7 @@ bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypePackId subTp, TypePac
|
|||||||
if (!get<GenericTypePack>(subTp))
|
if (!get<GenericTypePack>(subTp))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (TypePackId* m = env.mappedGenericPacks.find(subTp))
|
if (TypePackId* m = (DFInt::LuauTypeSolverRelease >= 644 ? env.getMappedPackBounds(subTp) : env.mappedGenericPacks.find(subTp)))
|
||||||
return *m == superTp;
|
return *m == superTp;
|
||||||
|
|
||||||
env.mappedGenericPacks[subTp] = superTp;
|
env.mappedGenericPacks[subTp] = superTp;
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
#include <ostream>
|
#include <ostream>
|
||||||
|
|
||||||
LUAU_FASTFLAG(DebugLuauMagicTypes)
|
LUAU_FASTFLAG(DebugLuauMagicTypes)
|
||||||
|
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
|
||||||
|
|
||||||
namespace Luau
|
namespace Luau
|
||||||
{
|
{
|
||||||
@ -3012,12 +3013,21 @@ PropertyType TypeChecker2::hasIndexTypeFromType(
|
|||||||
if (tt->indexer)
|
if (tt->indexer)
|
||||||
{
|
{
|
||||||
TypeId indexType = follow(tt->indexer->indexType);
|
TypeId indexType = follow(tt->indexer->indexType);
|
||||||
|
if (DFInt::LuauTypeSolverRelease >= 644)
|
||||||
|
{
|
||||||
|
TypeId givenType = module->internalTypes.addType(SingletonType{StringSingleton{prop}});
|
||||||
|
if (isSubtype(givenType, indexType, NotNull{module->getModuleScope().get()}, builtinTypes, *ice))
|
||||||
|
return {NormalizationResult::True, {tt->indexer->indexResultType}};
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (isPrim(indexType, PrimitiveType::String))
|
if (isPrim(indexType, PrimitiveType::String))
|
||||||
return {NormalizationResult::True, {tt->indexer->indexResultType}};
|
return {NormalizationResult::True, {tt->indexer->indexResultType}};
|
||||||
// If the indexer looks like { [any] : _} - the prop lookup should be allowed!
|
// If the indexer looks like { [any] : _} - the prop lookup should be allowed!
|
||||||
else if (get<AnyType>(indexType) || get<UnknownType>(indexType))
|
else if (get<AnyType>(indexType) || get<UnknownType>(indexType))
|
||||||
return {NormalizationResult::True, {tt->indexer->indexResultType}};
|
return {NormalizationResult::True, {tt->indexer->indexResultType}};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// if we are in a conditional context, we treat the property as present and `unknown` because
|
// if we are in a conditional context, we treat the property as present and `unknown` because
|
||||||
|
@ -37,6 +37,8 @@ LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyUseGuesserDepth, -1);
|
|||||||
|
|
||||||
LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies, false);
|
LUAU_FASTFLAGVARIABLE(DebugLuauLogTypeFamilies, false);
|
||||||
|
|
||||||
|
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
|
||||||
|
|
||||||
namespace Luau
|
namespace Luau
|
||||||
{
|
{
|
||||||
|
|
||||||
@ -669,8 +671,16 @@ TypeFunctionReductionResult<TypeId> lenTypeFunction(
|
|||||||
if (normTy->hasTopTable() || get<TableType>(normalizedOperand))
|
if (normTy->hasTopTable() || get<TableType>(normalizedOperand))
|
||||||
return {ctx->builtins->numberType, false, {}, {}};
|
return {ctx->builtins->numberType, false, {}, {}};
|
||||||
|
|
||||||
|
if (DFInt::LuauTypeSolverRelease >= 644)
|
||||||
|
{
|
||||||
|
if (auto result = tryDistributeTypeFunctionApp(lenTypeFunction, instance, typeParams, packParams, ctx))
|
||||||
|
return *result;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (auto result = tryDistributeTypeFunctionApp(notTypeFunction, instance, typeParams, packParams, ctx))
|
if (auto result = tryDistributeTypeFunctionApp(notTypeFunction, instance, typeParams, packParams, ctx))
|
||||||
return *result;
|
return *result;
|
||||||
|
}
|
||||||
|
|
||||||
// findMetatableEntry demands the ability to emit errors, so we must give it
|
// findMetatableEntry demands the ability to emit errors, so we must give it
|
||||||
// the necessary state to do that, even if we intend to just eat the errors.
|
// the necessary state to do that, even if we intend to just eat the errors.
|
||||||
@ -758,8 +768,16 @@ TypeFunctionReductionResult<TypeId> unmTypeFunction(
|
|||||||
if (normTy->isExactlyNumber())
|
if (normTy->isExactlyNumber())
|
||||||
return {ctx->builtins->numberType, false, {}, {}};
|
return {ctx->builtins->numberType, false, {}, {}};
|
||||||
|
|
||||||
|
if (DFInt::LuauTypeSolverRelease >= 644)
|
||||||
|
{
|
||||||
|
if (auto result = tryDistributeTypeFunctionApp(unmTypeFunction, instance, typeParams, packParams, ctx))
|
||||||
|
return *result;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
if (auto result = tryDistributeTypeFunctionApp(notTypeFunction, instance, typeParams, packParams, ctx))
|
if (auto result = tryDistributeTypeFunctionApp(notTypeFunction, instance, typeParams, packParams, ctx))
|
||||||
return *result;
|
return *result;
|
||||||
|
}
|
||||||
|
|
||||||
// findMetatableEntry demands the ability to emit errors, so we must give it
|
// findMetatableEntry demands the ability to emit errors, so we must give it
|
||||||
// the necessary state to do that, even if we intend to just eat the errors.
|
// the necessary state to do that, even if we intend to just eat the errors.
|
||||||
@ -2208,9 +2226,7 @@ TypeFunctionReductionResult<TypeId> indexFunctionImpl(
|
|||||||
TypeId indexerTy = follow(typeParams.at(1));
|
TypeId indexerTy = follow(typeParams.at(1));
|
||||||
|
|
||||||
if (isPending(indexerTy, ctx->solver))
|
if (isPending(indexerTy, ctx->solver))
|
||||||
{
|
|
||||||
return {std::nullopt, false, {indexerTy}, {}};
|
return {std::nullopt, false, {indexerTy}, {}};
|
||||||
}
|
|
||||||
|
|
||||||
std::shared_ptr<const NormalizedType> indexerNormTy = ctx->normalizer->normalize(indexerTy);
|
std::shared_ptr<const NormalizedType> indexerNormTy = ctx->normalizer->normalize(indexerTy);
|
||||||
|
|
||||||
|
@ -15,6 +15,9 @@
|
|||||||
|
|
||||||
LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2)
|
LUAU_FASTFLAG(LuauTraceTypesInNonstrictMode2)
|
||||||
LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel)
|
LUAU_FASTFLAG(LuauSetMetatableDoesNotTimeTravel)
|
||||||
|
LUAU_FASTFLAG(LuauAutocompleteNewSolverLimit)
|
||||||
|
LUAU_FASTINT(LuauTypeInferRecursionLimit)
|
||||||
|
LUAU_FASTFLAG(LuauUseNormalizeIntersectionLimit)
|
||||||
|
|
||||||
using namespace Luau;
|
using namespace Luau;
|
||||||
|
|
||||||
@ -3815,6 +3818,40 @@ TEST_CASE_FIXTURE(ACFixture, "autocomplete_response_perf1" * doctest::timeout(0.
|
|||||||
CHECK(ac.entryMap.count("Instance"));
|
CHECK(ac.entryMap.count("Instance"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(ACFixture, "autocomplete_subtyping_recursion_limit")
|
||||||
|
{
|
||||||
|
// TODO: in old solver, type resolve can't handle the type in this test without a stack overflow
|
||||||
|
if (!FFlag::LuauSolverV2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ScopedFastFlag luauAutocompleteNewSolverLimit{FFlag::LuauAutocompleteNewSolverLimit, true};
|
||||||
|
ScopedFastInt luauTypeInferRecursionLimit{FInt::LuauTypeInferRecursionLimit, 10};
|
||||||
|
|
||||||
|
const int parts = 100;
|
||||||
|
std::string source;
|
||||||
|
|
||||||
|
source += "function f()\n";
|
||||||
|
|
||||||
|
std::string prefix;
|
||||||
|
for (int i = 0; i < parts; i++)
|
||||||
|
formatAppend(prefix, "(nil|({a%d:number}&", i);
|
||||||
|
formatAppend(prefix, "(nil|{a%d:number})", parts);
|
||||||
|
for (int i = 0; i < parts; i++)
|
||||||
|
formatAppend(prefix, "))");
|
||||||
|
|
||||||
|
source += "local x1 : " + prefix + "\n";
|
||||||
|
source += "local y : {a1:number} = x@1\n";
|
||||||
|
|
||||||
|
source += "end\n";
|
||||||
|
|
||||||
|
check(source);
|
||||||
|
|
||||||
|
auto ac = autocomplete('1');
|
||||||
|
|
||||||
|
CHECK(ac.entryMap.count("true"));
|
||||||
|
CHECK(ac.entryMap.count("x1"));
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE_FIXTURE(ACFixture, "strict_mode_force")
|
TEST_CASE_FIXTURE(ACFixture, "strict_mode_force")
|
||||||
{
|
{
|
||||||
check(R"(
|
check(R"(
|
||||||
|
@ -16,6 +16,7 @@
|
|||||||
#include "doctest.h"
|
#include "doctest.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <limits>
|
||||||
#include <sstream>
|
#include <sstream>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
@ -27,6 +28,7 @@ LUAU_FASTFLAG(LuauSolverV2);
|
|||||||
LUAU_FASTFLAG(DebugLuauFreezeArena);
|
LUAU_FASTFLAG(DebugLuauFreezeArena);
|
||||||
LUAU_FASTFLAG(DebugLuauLogSolverToJsonFile)
|
LUAU_FASTFLAG(DebugLuauLogSolverToJsonFile)
|
||||||
LUAU_FASTFLAG(LuauDCRMagicFunctionTypeChecker);
|
LUAU_FASTFLAG(LuauDCRMagicFunctionTypeChecker);
|
||||||
|
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
|
||||||
|
|
||||||
extern std::optional<unsigned> randomSeed; // tests/main.cpp
|
extern std::optional<unsigned> randomSeed; // tests/main.cpp
|
||||||
|
|
||||||
@ -152,8 +154,12 @@ const Config& TestConfigResolver::getConfig(const ModuleName& name) const
|
|||||||
|
|
||||||
Fixture::Fixture(bool freeze, bool prepareAutocomplete)
|
Fixture::Fixture(bool freeze, bool prepareAutocomplete)
|
||||||
: sff_DebugLuauFreezeArena(FFlag::DebugLuauFreezeArena, freeze)
|
: sff_DebugLuauFreezeArena(FFlag::DebugLuauFreezeArena, freeze)
|
||||||
// In tests, we *always* want to register the extra magic functions for typechecking `string.format`.
|
|
||||||
, sff_LuauDCRMagicFunctionTypeChecker(FFlag::LuauDCRMagicFunctionTypeChecker, true)
|
, sff_LuauDCRMagicFunctionTypeChecker(FFlag::LuauDCRMagicFunctionTypeChecker, true)
|
||||||
|
// The first value of LuauTypeSolverRelease was 643, so as long as this is
|
||||||
|
// some number greater than 900 (5 years worth of releases), all tests that
|
||||||
|
// run under the new solver will run against all of the changes guarded by
|
||||||
|
// this flag.
|
||||||
|
, sff_LuauTypeSolverRelease(DFInt::LuauTypeSolverRelease, std::numeric_limits<int>::max())
|
||||||
, frontend(
|
, frontend(
|
||||||
&fileResolver,
|
&fileResolver,
|
||||||
&configResolver,
|
&configResolver,
|
||||||
|
@ -98,9 +98,37 @@ struct Fixture
|
|||||||
TypeId requireTypeAlias(const std::string& name);
|
TypeId requireTypeAlias(const std::string& name);
|
||||||
TypeId requireExportedType(const ModuleName& moduleName, const std::string& name);
|
TypeId requireExportedType(const ModuleName& moduleName, const std::string& name);
|
||||||
|
|
||||||
|
// TODO: Should this be in a container of some kind? Seems a little silly
|
||||||
|
// to have a bunch of flags sitting on the text fixture.
|
||||||
|
|
||||||
|
// We have a couple flags that are OK to set for all tests and, in some
|
||||||
|
// cases, cannot easily be flipped on or off on a per-test basis. For these
|
||||||
|
// we set them as part of constructing the test fixture.
|
||||||
|
|
||||||
|
/* From the original commit:
|
||||||
|
*
|
||||||
|
* > This enables arena freezing for all but two unit tests. Arena
|
||||||
|
* > freezing marks the `TypeArena`'s underlying memory as read-only,
|
||||||
|
* > raising an access violation whenever you mutate it. This is useful
|
||||||
|
* > for tracking down violations of Luau's memory model.
|
||||||
|
*/
|
||||||
ScopedFastFlag sff_DebugLuauFreezeArena;
|
ScopedFastFlag sff_DebugLuauFreezeArena;
|
||||||
|
|
||||||
|
/* Magic typechecker functions for the new solver are initialized when the
|
||||||
|
* typechecker frontend is initialized, which is done at the beginning of
|
||||||
|
* the test: we set this flag as part of the fixture as we always want to
|
||||||
|
* enable the magic functions for, say, `string.format`.
|
||||||
|
*/
|
||||||
ScopedFastFlag sff_LuauDCRMagicFunctionTypeChecker;
|
ScopedFastFlag sff_LuauDCRMagicFunctionTypeChecker;
|
||||||
|
|
||||||
|
/* While the new solver is being rolled out we are using a monotonically
|
||||||
|
* increasing version number to track new changes, we just set it to a
|
||||||
|
* sufficiently high number in tests to ensure that any guards in prod
|
||||||
|
* code pass in tests (so we don't accidentally reintroduce a bug before
|
||||||
|
* it's unflagged).
|
||||||
|
*/
|
||||||
|
ScopedFastInt sff_LuauTypeSolverRelease;
|
||||||
|
|
||||||
TestFileResolver fileResolver;
|
TestFileResolver fileResolver;
|
||||||
TestConfigResolver configResolver;
|
TestConfigResolver configResolver;
|
||||||
NullModuleResolver moduleResolver;
|
NullModuleResolver moduleResolver;
|
||||||
|
@ -939,14 +939,11 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "index_wait_for_pending_no_crash")
|
|||||||
Exp = 0,
|
Exp = 0,
|
||||||
MaxExp = 100
|
MaxExp = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
type Keys = index<typeof(PlayerData), keyof<typeof(PlayerData)>>
|
type Keys = index<typeof(PlayerData), keyof<typeof(PlayerData)>>
|
||||||
|
|
||||||
-- This function makes it think that there's going to be a pending expansion
|
-- This function makes it think that there's going to be a pending expansion
|
||||||
local function UpdateData(key: Keys, value)
|
local function UpdateData(key: Keys, value)
|
||||||
PlayerData[key] = value
|
PlayerData[key] = value
|
||||||
end
|
end
|
||||||
|
|
||||||
UpdateData("Coins", 2)
|
UpdateData("Coins", 2)
|
||||||
)");
|
)");
|
||||||
|
|
||||||
|
@ -1125,7 +1125,11 @@ TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments")
|
|||||||
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
|
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
|
||||||
REQUIRE(tm);
|
REQUIRE(tm);
|
||||||
CHECK_EQ("((number) -> number, string) -> number", toString(tm->wantedType));
|
CHECK_EQ("((number) -> number, string) -> number", toString(tm->wantedType));
|
||||||
if (FFlag::LuauInstantiateInSubtyping)
|
// The new solver does not attempt to instantiate generics here, so if
|
||||||
|
// either the instantiate in subtyping flag _or_ the new solver flags
|
||||||
|
// are set, assert that we're getting back the original generic
|
||||||
|
// function definition.
|
||||||
|
if (FFlag::LuauInstantiateInSubtyping || FFlag::LuauSolverV2)
|
||||||
CHECK_EQ("<a, b...>((a) -> (b...), a) -> (b...)", toString(tm->givenType));
|
CHECK_EQ("<a, b...>((a) -> (b...), a) -> (b...)", toString(tm->givenType));
|
||||||
else
|
else
|
||||||
CHECK_EQ("((number) -> number, number) -> number", toString(tm->givenType));
|
CHECK_EQ("((number) -> number, number) -> number", toString(tm->givenType));
|
||||||
@ -1148,7 +1152,11 @@ TEST_CASE_FIXTURE(Fixture, "instantiate_generic_function_in_assignments2")
|
|||||||
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
|
TypeMismatch* tm = get<TypeMismatch>(result.errors[0]);
|
||||||
REQUIRE(tm);
|
REQUIRE(tm);
|
||||||
CHECK_EQ("(string, string) -> number", toString(tm->wantedType));
|
CHECK_EQ("(string, string) -> number", toString(tm->wantedType));
|
||||||
if (FFlag::LuauInstantiateInSubtyping)
|
// The new solver does not attempt to instantiate generics here, so if
|
||||||
|
// either the instantiate in subtyping flag _or_ the new solver flags
|
||||||
|
// are set, assert that we're getting back the original generic
|
||||||
|
// function definition.
|
||||||
|
if (FFlag::LuauInstantiateInSubtyping || FFlag::LuauSolverV2)
|
||||||
CHECK_EQ("<a, b...>((a) -> (b...), a) -> (b...)", toString(tm->givenType));
|
CHECK_EQ("<a, b...>((a) -> (b...), a) -> (b...)", toString(tm->givenType));
|
||||||
else
|
else
|
||||||
CHECK_EQ("((string) -> number, string) -> number", toString(*tm->givenType));
|
CHECK_EQ("((string) -> number, string) -> number", toString(*tm->givenType));
|
||||||
@ -1587,4 +1595,31 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "generic_type_functions_work_in_subtyping")
|
|||||||
LUAU_REQUIRE_NO_ERRORS(result);
|
LUAU_REQUIRE_NO_ERRORS(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "generic_type_subtyping_nested_bounds_with_new_mappings")
|
||||||
|
{
|
||||||
|
// Test shows how going over mapped generics in a subtyping check can generate more mapped generics when making a subtyping check between bounds.
|
||||||
|
// It has previously caused iterator invalidation in the new solver, but this specific test doesn't trigger a UAF, only shows an example.
|
||||||
|
if (!FFlag::LuauSolverV2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CheckResult result = check(R"(
|
||||||
|
type Dispatch<A> = (A) -> ()
|
||||||
|
type BasicStateAction<S> = ((S) -> S) | S
|
||||||
|
|
||||||
|
function updateReducer<S, I, A>(reducer: (S, A) -> S, initialArg: I, init: ((I) -> S)?): (S, Dispatch<A>)
|
||||||
|
return 1 :: any
|
||||||
|
end
|
||||||
|
|
||||||
|
function basicStateReducer<S>(state: S, action: BasicStateAction<S>): S
|
||||||
|
return action
|
||||||
|
end
|
||||||
|
|
||||||
|
function updateState<S>(initialState: (() -> S) | S): (S, Dispatch<BasicStateAction<S>>)
|
||||||
|
return updateReducer(basicStateReducer, initialState)
|
||||||
|
end
|
||||||
|
)");
|
||||||
|
|
||||||
|
LUAU_REQUIRE_NO_ERRORS(result);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_SUITE_END();
|
TEST_SUITE_END();
|
||||||
|
@ -1576,7 +1576,9 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "compare_singleton_string_to_string")
|
|||||||
end
|
end
|
||||||
)");
|
)");
|
||||||
|
|
||||||
if (FFlag::LuauRemoveBadRelationalOperatorWarning)
|
// There is a flag to gate turning this off, and this warning is not
|
||||||
|
// implemented in the new solver, so assert there are no errors.
|
||||||
|
if (FFlag::LuauRemoveBadRelationalOperatorWarning || FFlag::LuauSolverV2)
|
||||||
LUAU_REQUIRE_NO_ERRORS(result);
|
LUAU_REQUIRE_NO_ERRORS(result);
|
||||||
else
|
else
|
||||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||||
|
@ -8,6 +8,7 @@
|
|||||||
#include "doctest.h"
|
#include "doctest.h"
|
||||||
|
|
||||||
LUAU_FASTFLAG(LuauSolverV2)
|
LUAU_FASTFLAG(LuauSolverV2)
|
||||||
|
LUAU_FASTFLAG(LuauUseNormalizeIntersectionLimit)
|
||||||
|
|
||||||
using namespace Luau;
|
using namespace Luau;
|
||||||
|
|
||||||
@ -2324,4 +2325,50 @@ end)
|
|||||||
)"));
|
)"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "refinements_table_intersection_limits" * doctest::timeout(0.5))
|
||||||
|
{
|
||||||
|
ScopedFastFlag LuauUseNormalizeIntersectionLimit{FFlag::LuauUseNormalizeIntersectionLimit, true};
|
||||||
|
|
||||||
|
CheckResult result = check(R"(
|
||||||
|
--!strict
|
||||||
|
type Dir = {
|
||||||
|
a: number?, b: number?, c: number?, d: number?, e: number?, f: number?,
|
||||||
|
g: number?, h: number?, i: number?, j: number?, k: number?, l: number?,
|
||||||
|
m: number?, n: number?, o: number?, p: number?, q: number?, r: number?,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function test(dirs: {Dir})
|
||||||
|
for k, dir in dirs
|
||||||
|
local success, message = pcall(function()
|
||||||
|
assert(dir.a == nil or type(dir.a) == "number")
|
||||||
|
assert(dir.b == nil or type(dir.b) == "number")
|
||||||
|
assert(dir.c == nil or type(dir.c) == "number")
|
||||||
|
assert(dir.d == nil or type(dir.d) == "number")
|
||||||
|
assert(dir.e == nil or type(dir.e) == "number")
|
||||||
|
assert(dir.f == nil or type(dir.f) == "number")
|
||||||
|
assert(dir.g == nil or type(dir.g) == "number")
|
||||||
|
assert(dir.h == nil or type(dir.h) == "number")
|
||||||
|
assert(dir.i == nil or type(dir.i) == "number")
|
||||||
|
assert(dir.j == nil or type(dir.j) == "number")
|
||||||
|
assert(dir.k == nil or type(dir.k) == "number")
|
||||||
|
assert(dir.l == nil or type(dir.l) == "number")
|
||||||
|
assert(dir.m == nil or type(dir.m) == "number")
|
||||||
|
assert(dir.n == nil or type(dir.n) == "number")
|
||||||
|
assert(dir.o == nil or type(dir.o) == "number")
|
||||||
|
assert(dir.p == nil or type(dir.p) == "number")
|
||||||
|
assert(dir.q == nil or type(dir.q) == "number")
|
||||||
|
assert(dir.r == nil or type(dir.r) == "number")
|
||||||
|
assert(dir.t == nil or type(dir.t) == "number")
|
||||||
|
assert(dir.u == nil or type(dir.u) == "number")
|
||||||
|
assert(dir.v == nil or type(dir.v) == "number")
|
||||||
|
local checkpoint = dir
|
||||||
|
|
||||||
|
checkpoint.w = 1
|
||||||
|
end)
|
||||||
|
assert(success)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
)");
|
||||||
|
}
|
||||||
|
|
||||||
TEST_SUITE_END();
|
TEST_SUITE_END();
|
||||||
|
@ -334,6 +334,27 @@ TEST_CASE_FIXTURE(Fixture, "table_properties_alias_or_parens_is_indexer")
|
|||||||
CHECK_EQ("Cannot have more than one table indexer", toString(result.errors[0]));
|
CHECK_EQ("Cannot have more than one table indexer", toString(result.errors[0]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(Fixture, "indexer_can_be_union_of_singletons")
|
||||||
|
{
|
||||||
|
if (!FFlag::LuauSolverV2)
|
||||||
|
return;
|
||||||
|
|
||||||
|
CheckResult result = check(R"(
|
||||||
|
type Target = "A" | "B"
|
||||||
|
|
||||||
|
type Test = {[Target]: number}
|
||||||
|
|
||||||
|
local test: Test = {}
|
||||||
|
|
||||||
|
test.A = 2
|
||||||
|
test.C = 4
|
||||||
|
)");
|
||||||
|
|
||||||
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||||
|
|
||||||
|
CHECK(8 == result.errors[0].location.begin.line);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes")
|
TEST_CASE_FIXTURE(Fixture, "table_properties_type_error_escapes")
|
||||||
{
|
{
|
||||||
CheckResult result = check(R"(
|
CheckResult result = check(R"(
|
||||||
|
@ -21,6 +21,7 @@ LUAU_FASTFLAG(LuauFixIndexerSubtypingOrdering)
|
|||||||
LUAU_FASTFLAG(LuauAcceptIndexingTableUnionsIntersections)
|
LUAU_FASTFLAG(LuauAcceptIndexingTableUnionsIntersections)
|
||||||
|
|
||||||
LUAU_DYNAMIC_FASTFLAG(LuauImproveNonFunctionCallError)
|
LUAU_DYNAMIC_FASTFLAG(LuauImproveNonFunctionCallError)
|
||||||
|
LUAU_DYNAMIC_FASTINT(LuauTypeSolverRelease)
|
||||||
|
|
||||||
TEST_SUITE_BEGIN("TableTests");
|
TEST_SUITE_BEGIN("TableTests");
|
||||||
|
|
||||||
@ -2653,12 +2654,15 @@ local y = #x
|
|||||||
|
|
||||||
TEST_CASE_FIXTURE(Fixture, "length_operator_union_errors")
|
TEST_CASE_FIXTURE(Fixture, "length_operator_union_errors")
|
||||||
{
|
{
|
||||||
|
ScopedFastFlag _{FFlag::LuauSolverV2, true};
|
||||||
|
|
||||||
CheckResult result = check(R"(
|
CheckResult result = check(R"(
|
||||||
local x: {number} | number | string
|
local x: {number} | number | string
|
||||||
local y = #x
|
local y = #x
|
||||||
)");
|
)");
|
||||||
|
|
||||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
// CLI-119936: This shouldn't double error but does under the new solver.
|
||||||
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE_FIXTURE(BuiltinsFixture, "dont_hang_when_trying_to_look_up_in_cyclic_metatable_index")
|
TEST_CASE_FIXTURE(BuiltinsFixture, "dont_hang_when_trying_to_look_up_in_cyclic_metatable_index")
|
||||||
@ -3261,22 +3265,22 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "table_call_metamethod_must_be_callable")
|
|||||||
|
|
||||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||||
|
|
||||||
if (FFlag::LuauSolverV2)
|
if (!FFlag::LuauSolverV2)
|
||||||
{
|
|
||||||
if (DFFlag::LuauImproveNonFunctionCallError)
|
|
||||||
CHECK("Cannot call a value of type a" == toString(result.errors[0]));
|
|
||||||
else
|
|
||||||
CHECK("Cannot call non-function { @metatable { __call: number }, { } }" == toString(result.errors[0]));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
TypeError e{
|
TypeError e{
|
||||||
Location{{5, 20}, {5, 21}},
|
Location{{5, 20}, {5, 21}},
|
||||||
CannotCallNonFunction{builtinTypes->numberType},
|
CannotCallNonFunction{builtinTypes->numberType},
|
||||||
};
|
};
|
||||||
|
|
||||||
CHECK(result.errors[0] == e);
|
CHECK(result.errors[0] == e);
|
||||||
}
|
}
|
||||||
|
else if (DFFlag::LuauImproveNonFunctionCallError)
|
||||||
|
{
|
||||||
|
CHECK("Cannot call a value of type a" == toString(result.errors[0]));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CHECK("Cannot call non-function a" == toString(result.errors[0]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE_FIXTURE(BuiltinsFixture, "table_call_metamethod_generic")
|
TEST_CASE_FIXTURE(BuiltinsFixture, "table_call_metamethod_generic")
|
||||||
@ -4832,4 +4836,19 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "indexing_branching_table2")
|
|||||||
CHECK("any" == toString(requireType("test2")));
|
CHECK("any" == toString(requireType("test2")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_CASE_FIXTURE(BuiltinsFixture, "length_of_array_is_number")
|
||||||
|
{
|
||||||
|
CheckResult result = check(R"(
|
||||||
|
local function TestFunc(ranges: {number}): number
|
||||||
|
if true then
|
||||||
|
ranges = {} :: {number}
|
||||||
|
end
|
||||||
|
local numRanges: number = #ranges
|
||||||
|
return numRanges
|
||||||
|
end
|
||||||
|
)");
|
||||||
|
|
||||||
|
LUAU_REQUIRE_NO_ERRORS(result);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_SUITE_END();
|
TEST_SUITE_END();
|
||||||
|
@ -419,6 +419,9 @@ TEST_CASE_FIXTURE(Fixture, "optional_assignment_errors_2")
|
|||||||
|
|
||||||
TEST_CASE_FIXTURE(Fixture, "optional_length_error")
|
TEST_CASE_FIXTURE(Fixture, "optional_length_error")
|
||||||
{
|
{
|
||||||
|
|
||||||
|
ScopedFastFlag _{FFlag::LuauSolverV2, true};
|
||||||
|
|
||||||
CheckResult result = check(R"(
|
CheckResult result = check(R"(
|
||||||
type A = {number}
|
type A = {number}
|
||||||
function f(a: A?)
|
function f(a: A?)
|
||||||
@ -426,8 +429,10 @@ TEST_CASE_FIXTURE(Fixture, "optional_length_error")
|
|||||||
end
|
end
|
||||||
)");
|
)");
|
||||||
|
|
||||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
// CLI-119936: This shouldn't double error but does under the new solver.
|
||||||
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[0]));
|
LUAU_REQUIRE_ERROR_COUNT(2, result);
|
||||||
|
CHECK_EQ("Operator '#' could not be applied to operand of type A?; there is no corresponding overload for __len", toString(result.errors[0]));
|
||||||
|
CHECK_EQ("Value of type 'A?' could be nil", toString(result.errors[1]));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE_FIXTURE(Fixture, "optional_missing_key_error_details")
|
TEST_CASE_FIXTURE(Fixture, "optional_missing_key_error_details")
|
||||||
@ -638,8 +643,9 @@ TEST_CASE_FIXTURE(Fixture, "indexing_into_a_cyclic_union_doesnt_crash")
|
|||||||
)");
|
)");
|
||||||
|
|
||||||
// this is a cyclic union of number arrays, so it _is_ a table, even if it's a nonsense type.
|
// this is a cyclic union of number arrays, so it _is_ a table, even if it's a nonsense type.
|
||||||
// no need to generate a NotATable error here.
|
// no need to generate a NotATable error here. The new solver automatically handles this and
|
||||||
if (FFlag::LuauAcceptIndexingTableUnionsIntersections)
|
// correctly reports no errors.
|
||||||
|
if (FFlag::LuauAcceptIndexingTableUnionsIntersections || FFlag::LuauSolverV2)
|
||||||
LUAU_REQUIRE_NO_ERRORS(result);
|
LUAU_REQUIRE_NO_ERRORS(result);
|
||||||
else
|
else
|
||||||
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
LUAU_REQUIRE_ERROR_COUNT(1, result);
|
||||||
|
Loading…
Reference in New Issue
Block a user