Sync to upstream/release/599

This commit is contained in:
Lily Brown 2023-10-13 12:38:31 -07:00
parent 22e3d1fa46
commit 34efdb3b0a
60 changed files with 2442 additions and 952 deletions

View file

@ -69,6 +69,12 @@ struct ConstraintGraphBuilder
// This is null when the CGB is initially constructed. // This is null when the CGB is initially constructed.
Scope* rootScope; Scope* rootScope;
// During constraint generation, we only populate the Scope::bindings
// property for annotated symbols. Unannotated symbols must be handled in a
// postprocessing step because we do not yet have the full breadcrumb graph.
// We queue them up here.
std::vector<std::tuple<Scope*, Symbol, BreadcrumbId>> inferredBindings;
// Constraints that go straight to the solver. // Constraints that go straight to the solver.
std::vector<ConstraintPtr> constraints; std::vector<ConstraintPtr> constraints;
@ -205,8 +211,6 @@ private:
Inference check(const ScopePtr& scope, AstExprTable* expr, std::optional<TypeId> expectedType); Inference check(const ScopePtr& scope, AstExprTable* expr, std::optional<TypeId> expectedType);
std::tuple<TypeId, TypeId, RefinementId> checkBinary(const ScopePtr& scope, AstExprBinary* binary, std::optional<TypeId> expectedType); std::tuple<TypeId, TypeId, RefinementId> checkBinary(const ScopePtr& scope, AstExprBinary* binary, std::optional<TypeId> expectedType);
std::vector<TypeId> checkLValues(const ScopePtr& scope, AstArray<AstExpr*> exprs);
TypeId checkLValue(const ScopePtr& scope, AstExpr* expr); TypeId checkLValue(const ScopePtr& scope, AstExpr* expr);
TypeId checkLValue(const ScopePtr& scope, AstExprLocal* local); TypeId checkLValue(const ScopePtr& scope, AstExprLocal* local);
TypeId checkLValue(const ScopePtr& scope, AstExprGlobal* global); TypeId checkLValue(const ScopePtr& scope, AstExprGlobal* global);
@ -303,6 +307,8 @@ private:
*/ */
void prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program); void prepopulateGlobalScope(const ScopePtr& globalScope, AstStatBlock* program);
void fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block);
/** Given a function type annotation, return a vector describing the expected types of the calls to the function /** Given a function type annotation, return a vector describing the expected types of the calls to the function
* For example, calling a function with annotation ((number) -> string & ((string) -> number)) * For example, calling a function with annotation ((number) -> string & ((string) -> number))
* yields a vector of size 1, with value: [number | string] * yields a vector of size 1, with value: [number | string]

View file

@ -120,12 +120,12 @@ private:
BreadcrumbId visitExpr(DfgScope* scope, AstExprInterpString* i); BreadcrumbId visitExpr(DfgScope* scope, AstExprInterpString* i);
BreadcrumbId visitExpr(DfgScope* scope, AstExprError* error); BreadcrumbId visitExpr(DfgScope* scope, AstExprError* error);
void visitLValue(DfgScope* scope, AstExpr* e); void visitLValue(DfgScope* scope, AstExpr* e, BreadcrumbId bc);
void visitLValue(DfgScope* scope, AstExprLocal* l); void visitLValue(DfgScope* scope, AstExprLocal* l, BreadcrumbId bc);
void visitLValue(DfgScope* scope, AstExprGlobal* g); void visitLValue(DfgScope* scope, AstExprGlobal* g, BreadcrumbId bc);
void visitLValue(DfgScope* scope, AstExprIndexName* i); void visitLValue(DfgScope* scope, AstExprIndexName* i, BreadcrumbId bc);
void visitLValue(DfgScope* scope, AstExprIndexExpr* i); void visitLValue(DfgScope* scope, AstExprIndexExpr* i, BreadcrumbId bc);
void visitLValue(DfgScope* scope, AstExprError* e); void visitLValue(DfgScope* scope, AstExprError* e, BreadcrumbId bc);
void visitType(DfgScope* scope, AstType* t); void visitType(DfgScope* scope, AstType* t);
void visitType(DfgScope* scope, AstTypeReference* r); void visitType(DfgScope* scope, AstTypeReference* r);

View file

@ -358,13 +358,23 @@ struct PackWhereClauseNeeded
bool operator==(const PackWhereClauseNeeded& rhs) const; bool operator==(const PackWhereClauseNeeded& rhs) const;
}; };
struct CheckedFunctionCallError
{
TypeId expected;
TypeId passed;
std::string checkedFunctionName;
// TODO: make this a vector<argumentIndices>
size_t argumentIndex;
bool operator==(const CheckedFunctionCallError& rhs) const;
};
using TypeErrorData = Variant<TypeMismatch, UnknownSymbol, UnknownProperty, NotATable, CannotExtendTable, OnlyTablesCanHaveMethods, using TypeErrorData = Variant<TypeMismatch, UnknownSymbol, UnknownProperty, NotATable, CannotExtendTable, OnlyTablesCanHaveMethods,
DuplicateTypeDefinition, CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire, DuplicateTypeDefinition, CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire,
IncorrectGenericParameterCount, SyntaxError, CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError, InternalError, IncorrectGenericParameterCount, SyntaxError, CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError, InternalError,
CannotCallNonFunction, ExtraInformation, DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning, CannotCallNonFunction, ExtraInformation, DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning,
DuplicateGenericParameter, CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty, DuplicateGenericParameter, CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty,
TypesAreUnrelated, NormalizationTooComplex, TypePackMismatch, DynamicPropertyLookupOnClassesUnsafe, UninhabitedTypeFamily, TypesAreUnrelated, NormalizationTooComplex, TypePackMismatch, DynamicPropertyLookupOnClassesUnsafe, UninhabitedTypeFamily,
UninhabitedTypePackFamily, WhereClauseNeeded, PackWhereClauseNeeded>; UninhabitedTypePackFamily, WhereClauseNeeded, PackWhereClauseNeeded, CheckedFunctionCallError>;
struct TypeErrorSummary struct TypeErrorSummary
{ {

View file

@ -245,12 +245,12 @@ public:
std::vector<ModuleName> moduleQueue; std::vector<ModuleName> moduleQueue;
}; };
ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle>& requireCycles, NotNull<BuiltinTypes> builtinTypes, ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vector<RequireCycle>& requireCycles, NotNull<BuiltinTypes> builtinTypes,
NotNull<InternalErrorReporter> iceHandler, NotNull<ModuleResolver> moduleResolver, NotNull<FileResolver> fileResolver, NotNull<InternalErrorReporter> iceHandler, NotNull<ModuleResolver> moduleResolver, NotNull<FileResolver> fileResolver,
const ScopePtr& globalScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options, const ScopePtr& globalScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options,
TypeCheckLimits limits); TypeCheckLimits limits);
ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle>& requireCycles, NotNull<BuiltinTypes> builtinTypes, ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vector<RequireCycle>& requireCycles, NotNull<BuiltinTypes> builtinTypes,
NotNull<InternalErrorReporter> iceHandler, NotNull<ModuleResolver> moduleResolver, NotNull<FileResolver> fileResolver, NotNull<InternalErrorReporter> iceHandler, NotNull<ModuleResolver> moduleResolver, NotNull<FileResolver> fileResolver,
const ScopePtr& globalScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options, const ScopePtr& globalScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options,
TypeCheckLimits limits, bool recordJsonLog); TypeCheckLimits limits, bool recordJsonLog);

View file

@ -3,13 +3,17 @@
#include "Luau/Module.h" #include "Luau/Module.h"
#include "Luau/NotNull.h" #include "Luau/NotNull.h"
#include "Luau/DataFlowGraph.h"
namespace Luau namespace Luau
{ {
struct BuiltinTypes; struct BuiltinTypes;
struct UnifierSharedState;
struct TypeCheckLimits;
void checkNonStrict(NotNull<BuiltinTypes> builtinTypes, NotNull<InternalErrorReporter> ice, NotNull<UnifierSharedState> unifierState,
NotNull<const DataFlowGraph> dfg, NotNull<TypeCheckLimits> limits, const SourceModule& sourceModule, Module* module);
void checkNonStrict(NotNull<BuiltinTypes> builtinTypes, Module* module);
} // namespace Luau } // namespace Luau

View file

@ -3,6 +3,7 @@
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/TypePack.h" #include "Luau/TypePack.h"
#include "Luau/TypePairHash.h"
#include "Luau/UnifierSharedState.h" #include "Luau/UnifierSharedState.h"
#include <vector> #include <vector>
@ -28,6 +29,7 @@ struct SubtypingResult
bool isSubtype = false; bool isSubtype = false;
bool isErrorSuppressing = false; bool isErrorSuppressing = false;
bool normalizationTooComplex = false; bool normalizationTooComplex = false;
bool isCacheable = true;
SubtypingResult& andAlso(const SubtypingResult& other); SubtypingResult& andAlso(const SubtypingResult& other);
SubtypingResult& orElse(const SubtypingResult& other); SubtypingResult& orElse(const SubtypingResult& other);
@ -38,6 +40,24 @@ struct SubtypingResult
static SubtypingResult any(const std::vector<SubtypingResult>& results); static SubtypingResult any(const std::vector<SubtypingResult>& results);
}; };
struct SubtypingEnvironment
{
struct GenericBounds
{
DenseHashSet<TypeId> lowerBound{nullptr};
DenseHashSet<TypeId> upperBound{nullptr};
};
/*
* 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.
*/
DenseHashMap<TypeId, GenericBounds> mappedGenerics{nullptr};
DenseHashMap<TypePackId, TypePackId> mappedGenericPacks{nullptr};
DenseHashMap<std::pair<TypeId, TypeId>, SubtypingResult, TypePairHash> ephemeralCache{{}};
};
struct Subtyping struct Subtyping
{ {
NotNull<BuiltinTypes> builtinTypes; NotNull<BuiltinTypes> builtinTypes;
@ -55,29 +75,25 @@ struct Subtyping
Variance variance = Variance::Covariant; Variance variance = Variance::Covariant;
struct GenericBounds
{
DenseHashSet<TypeId> lowerBound{nullptr};
DenseHashSet<TypeId> upperBound{nullptr};
};
/*
* 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.
*/
DenseHashMap<TypeId, GenericBounds> mappedGenerics{nullptr};
DenseHashMap<TypePackId, TypePackId> mappedGenericPacks{nullptr};
using SeenSet = std::unordered_set<std::pair<TypeId, TypeId>, TypeIdPairHash>; using SeenSet = std::unordered_set<std::pair<TypeId, TypeId>, TypeIdPairHash>;
SeenSet seenTypes; SeenSet seenTypes;
Subtyping(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> typeArena, NotNull<Normalizer> normalizer,
NotNull<InternalErrorReporter> iceReporter, NotNull<Scope> scope);
Subtyping(const Subtyping&) = delete; Subtyping(const Subtyping&) = delete;
Subtyping& operator=(const Subtyping&) = delete; Subtyping& operator=(const Subtyping&) = delete;
Subtyping(Subtyping&&) = default; Subtyping(Subtyping&&) = default;
Subtyping& operator=(Subtyping&&) = default; Subtyping& operator=(Subtyping&&) = default;
// Only used by unit tests to test that the cache works.
const DenseHashMap<std::pair<TypeId, TypeId>, SubtypingResult, TypePairHash>& peekCache() const
{
return resultCache;
}
// TODO cache // TODO cache
// TODO cyclic types // TODO cyclic types
// TODO recursion limits // TODO recursion limits
@ -86,58 +102,63 @@ struct Subtyping
SubtypingResult isSubtype(TypePackId subTy, TypePackId superTy); SubtypingResult isSubtype(TypePackId subTy, TypePackId superTy);
private: private:
SubtypingResult isCovariantWith(TypeId subTy, TypeId superTy); DenseHashMap<std::pair<TypeId, TypeId>, SubtypingResult, TypePairHash> resultCache{{}};
SubtypingResult isCovariantWith(TypePackId subTy, TypePackId superTy);
SubtypingResult cache(SubtypingEnvironment& env, SubtypingResult res, TypeId subTy, TypeId superTy);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypePackId subTy, TypePackId superTy);
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingResult isContravariantWith(SubTy&& subTy, SuperTy&& superTy); SubtypingResult isContravariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy);
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingResult isInvariantWith(SubTy&& subTy, SuperTy&& superTy); SubtypingResult isInvariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy);
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingResult isCovariantWith(const TryPair<const SubTy*, const SuperTy*>& pair); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TryPair<const SubTy*, const SuperTy*>& pair);
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingResult isContravariantWith(const TryPair<const SubTy*, const SuperTy*>& pair); SubtypingResult isContravariantWith(SubtypingEnvironment& env, const TryPair<const SubTy*, const SuperTy*>& pair);
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingResult isInvariantWith(const TryPair<const SubTy*, const SuperTy*>& pair); SubtypingResult isInvariantWith(SubtypingEnvironment& env, const TryPair<const SubTy*, const SuperTy*>& pair);
SubtypingResult isCovariantWith(TypeId subTy, const UnionType* superUnion); SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const UnionType* superUnion);
SubtypingResult isCovariantWith(const UnionType* subUnion, TypeId superTy); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const UnionType* subUnion, TypeId superTy);
SubtypingResult isCovariantWith(TypeId subTy, const IntersectionType* superIntersection); SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const IntersectionType* superIntersection);
SubtypingResult isCovariantWith(const IntersectionType* subIntersection, TypeId superTy); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const IntersectionType* subIntersection, TypeId superTy);
SubtypingResult isCovariantWith(const NegationType* subNegation, TypeId superTy); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NegationType* subNegation, TypeId superTy);
SubtypingResult isCovariantWith(const TypeId subTy, const NegationType* superNegation); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TypeId subTy, const NegationType* superNegation);
SubtypingResult isCovariantWith(const PrimitiveType* subPrim, const PrimitiveType* superPrim); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const PrimitiveType* superPrim);
SubtypingResult isCovariantWith(const SingletonType* subSingleton, const PrimitiveType* superPrim); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const PrimitiveType* superPrim);
SubtypingResult isCovariantWith(const SingletonType* subSingleton, const SingletonType* superSingleton); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const SingletonType* superSingleton);
SubtypingResult isCovariantWith(const TableType* subTable, const TableType* superTable); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TableType* subTable, const TableType* superTable);
SubtypingResult isCovariantWith(const MetatableType* subMt, const MetatableType* superMt); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const MetatableType* superMt);
SubtypingResult isCovariantWith(const MetatableType* subMt, const TableType* superTable); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable);
SubtypingResult isCovariantWith(const ClassType* subClass, const ClassType* superClass); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const ClassType* superClass);
SubtypingResult isCovariantWith(const ClassType* subClass, const TableType* superTable); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const TableType* superTable);
SubtypingResult isCovariantWith(const FunctionType* subFunction, const FunctionType* superFunction); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const FunctionType* subFunction, const FunctionType* superFunction);
SubtypingResult isCovariantWith(const PrimitiveType* subPrim, const TableType* superTable); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const TableType* superTable);
SubtypingResult isCovariantWith(const SingletonType* subSingleton, const TableType* superTable); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const TableType* superTable);
SubtypingResult isCovariantWith(const TableIndexer& subIndexer, const TableIndexer& superIndexer); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TableIndexer& subIndexer, const TableIndexer& superIndexer);
SubtypingResult isCovariantWith(const NormalizedType* subNorm, const NormalizedType* superNorm); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm);
SubtypingResult isCovariantWith(const NormalizedClassType& subClass, const NormalizedClassType& superClass); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const NormalizedClassType& superClass);
SubtypingResult isCovariantWith(const NormalizedClassType& subClass, const TypeIds& superTables); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const TypeIds& superTables);
SubtypingResult isCovariantWith(const NormalizedStringType& subString, const NormalizedStringType& superString); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedStringType& subString, const NormalizedStringType& superString);
SubtypingResult isCovariantWith(const NormalizedStringType& subString, const TypeIds& superTables); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedStringType& subString, const TypeIds& superTables);
SubtypingResult isCovariantWith(const NormalizedFunctionType& subFunction, const NormalizedFunctionType& superFunction); SubtypingResult isCovariantWith(
SubtypingResult isCovariantWith(const TypeIds& subTypes, const TypeIds& superTypes); SubtypingEnvironment& env, const NormalizedFunctionType& subFunction, const NormalizedFunctionType& superFunction);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TypeIds& subTypes, const TypeIds& superTypes);
SubtypingResult isCovariantWith(const VariadicTypePack* subVariadic, const VariadicTypePack* superVariadic); SubtypingResult isCovariantWith(SubtypingEnvironment& env, const VariadicTypePack* subVariadic, const VariadicTypePack* superVariadic);
bool bindGeneric(TypeId subTp, TypeId superTp); bool bindGeneric(SubtypingEnvironment& env, TypeId subTp, TypeId superTp);
bool bindGeneric(TypePackId subTp, TypePackId superTp); bool bindGeneric(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp);
template<typename T, typename Container> template<typename T, typename Container>
TypeId makeAggregateType(const Container& container, TypeId orElse); TypeId makeAggregateType(const Container& container, TypeId orElse);

View file

@ -93,7 +93,7 @@ struct TypeFamily
std::string name; std::string name;
/// The reducer function for the type family. /// The reducer function for the type family.
std::function<TypeFamilyReductionResult<TypeId>(std::vector<TypeId>, std::vector<TypePackId>, NotNull<TypeFamilyContext>)> reducer; std::function<TypeFamilyReductionResult<TypeId>(const std::vector<TypeId>&, const std::vector<TypePackId>&, NotNull<TypeFamilyContext>)> reducer;
}; };
/// Represents a type function that may be applied to map a series of types and /// Represents a type function that may be applied to map a series of types and
@ -105,7 +105,7 @@ struct TypePackFamily
std::string name; std::string name;
/// The reducer function for the type pack family. /// The reducer function for the type pack family.
std::function<TypeFamilyReductionResult<TypePackId>(std::vector<TypeId>, std::vector<TypePackId>, NotNull<TypeFamilyContext>)> reducer; std::function<TypeFamilyReductionResult<TypePackId>(const std::vector<TypeId>&, const std::vector<TypePackId>&, NotNull<TypeFamilyContext>)> reducer;
}; };
struct FamilyGraphReductionResult struct FamilyGraphReductionResult
@ -149,6 +149,8 @@ struct BuiltinTypeFamilies
{ {
BuiltinTypeFamilies(); BuiltinTypeFamilies();
TypeFamily notFamily;
TypeFamily addFamily; TypeFamily addFamily;
TypeFamily subFamily; TypeFamily subFamily;
TypeFamily mulFamily; TypeFamily mulFamily;
@ -157,9 +159,15 @@ struct BuiltinTypeFamilies
TypeFamily powFamily; TypeFamily powFamily;
TypeFamily modFamily; TypeFamily modFamily;
TypeFamily concatFamily;
TypeFamily andFamily; TypeFamily andFamily;
TypeFamily orFamily; TypeFamily orFamily;
TypeFamily ltFamily;
TypeFamily leFamily;
TypeFamily eqFamily;
void addToScope(NotNull<TypeArena> arena, NotNull<Scope> scope) const; void addToScope(NotNull<TypeArena> arena, NotNull<Scope> scope) const;
}; };

View file

@ -0,0 +1,34 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#pragma once
#include "Type.h"
#include <utility>
namespace Luau
{
struct TypePairHash
{
size_t hashOne(TypeId key) const
{
return (uintptr_t(key) >> 4) ^ (uintptr_t(key) >> 9);
}
size_t hashOne(TypePackId key) const
{
return (uintptr_t(key) >> 4) ^ (uintptr_t(key) >> 9);
}
size_t operator()(const std::pair<TypeId, TypeId>& x) const
{
return hashOne(x.first) ^ (hashOne(x.second) << 1);
}
size_t operator()(const std::pair<TypePackId, TypePackId>& x) const
{
return hashOne(x.first) ^ (hashOne(x.second) << 1);
}
};
} // namespace Luau

View file

@ -5,6 +5,7 @@
#include "Luau/DenseHash.h" #include "Luau/DenseHash.h"
#include "Luau/NotNull.h" #include "Luau/NotNull.h"
#include "Type.h" #include "Type.h"
#include "TypePairHash.h"
#include "TypeCheckLimits.h" #include "TypeCheckLimits.h"
#include "TypeChecker2.h" #include "TypeChecker2.h"
@ -29,29 +30,6 @@ enum class OccursCheckResult
Fail Fail
}; };
struct TypePairHash
{
size_t hashOne(Luau::TypeId key) const
{
return (uintptr_t(key) >> 4) ^ (uintptr_t(key) >> 9);
}
size_t hashOne(Luau::TypePackId key) const
{
return (uintptr_t(key) >> 4) ^ (uintptr_t(key) >> 9);
}
size_t operator()(const std::pair<Luau::TypeId, Luau::TypeId>& x) const
{
return hashOne(x.first) ^ (hashOne(x.second) << 1);
}
size_t operator()(const std::pair<Luau::TypePackId, Luau::TypePackId>& x) const
{
return hashOne(x.first) ^ (hashOne(x.second) << 1);
}
};
struct Unifier2 struct Unifier2
{ {
NotNull<TypeArena> arena; NotNull<TypeArena> arena;

View file

@ -8,7 +8,8 @@
#include <math.h> #include <math.h>
LUAU_FASTFLAG(LuauFloorDivision) LUAU_FASTFLAG(LuauFloorDivision);
LUAU_FASTFLAG(LuauClipExtraHasEndProps);
namespace Luau namespace Luau
{ {
@ -376,7 +377,8 @@ struct AstJsonEncoder : public AstVisitor
PROP(body); PROP(body);
PROP(functionDepth); PROP(functionDepth);
PROP(debugname); PROP(debugname);
PROP(hasEnd); if (!FFlag::LuauClipExtraHasEndProps)
write("hasEnd", node->DEPRECATED_hasEnd);
}); });
} }
@ -574,6 +576,11 @@ struct AstJsonEncoder : public AstVisitor
void write(class AstStatBlock* node) void write(class AstStatBlock* node)
{ {
writeNode(node, "AstStatBlock", [&]() { writeNode(node, "AstStatBlock", [&]() {
if (FFlag::LuauClipExtraHasEndProps)
{
writeRaw(",\"hasEnd\":");
write(node->hasEnd);
}
writeRaw(",\"body\":["); writeRaw(",\"body\":[");
bool comma = false; bool comma = false;
for (AstStat* stat : node->body) for (AstStat* stat : node->body)
@ -597,7 +604,8 @@ struct AstJsonEncoder : public AstVisitor
if (node->elsebody) if (node->elsebody)
PROP(elsebody); PROP(elsebody);
write("hasThen", node->thenLocation.has_value()); write("hasThen", node->thenLocation.has_value());
PROP(hasEnd); if (!FFlag::LuauClipExtraHasEndProps)
write("hasEnd", node->DEPRECATED_hasEnd);
}); });
} }
@ -607,7 +615,8 @@ struct AstJsonEncoder : public AstVisitor
PROP(condition); PROP(condition);
PROP(body); PROP(body);
PROP(hasDo); PROP(hasDo);
PROP(hasEnd); if (!FFlag::LuauClipExtraHasEndProps)
write("hasEnd", node->DEPRECATED_hasEnd);
}); });
} }
@ -616,7 +625,8 @@ struct AstJsonEncoder : public AstVisitor
writeNode(node, "AstStatRepeat", [&]() { writeNode(node, "AstStatRepeat", [&]() {
PROP(condition); PROP(condition);
PROP(body); PROP(body);
PROP(hasUntil); if (!FFlag::LuauClipExtraHasEndProps)
write("hasUntil", node->DEPRECATED_hasUntil);
}); });
} }
@ -662,7 +672,8 @@ struct AstJsonEncoder : public AstVisitor
PROP(step); PROP(step);
PROP(body); PROP(body);
PROP(hasDo); PROP(hasDo);
PROP(hasEnd); if (!FFlag::LuauClipExtraHasEndProps)
write("hasEnd", node->DEPRECATED_hasEnd);
}); });
} }
@ -674,7 +685,8 @@ struct AstJsonEncoder : public AstVisitor
PROP(body); PROP(body);
PROP(hasIn); PROP(hasIn);
PROP(hasDo); PROP(hasDo);
PROP(hasEnd); if (!FFlag::LuauClipExtraHasEndProps)
write("hasEnd", node->DEPRECATED_hasEnd);
}); });
} }

View file

@ -12,8 +12,9 @@
#include <unordered_set> #include <unordered_set>
#include <utility> #include <utility>
LUAU_FASTFLAG(DebugLuauReadWriteProperties) LUAU_FASTFLAG(DebugLuauReadWriteProperties);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteDoEnd, false) LUAU_FASTFLAG(LuauClipExtraHasEndProps);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteDoEnd, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteStringLiteralBounds, false); LUAU_FASTFLAGVARIABLE(LuauAutocompleteStringLiteralBounds, false);
static const std::unordered_set<std::string> kStatementStartingKeywords = { static const std::unordered_set<std::string> kStatementStartingKeywords = {
@ -1055,22 +1056,56 @@ static AutocompleteEntryMap autocompleteStatement(
for (const auto& kw : kStatementStartingKeywords) for (const auto& kw : kStatementStartingKeywords)
result.emplace(kw, AutocompleteEntry{AutocompleteEntryKind::Keyword}); result.emplace(kw, AutocompleteEntry{AutocompleteEntryKind::Keyword});
for (auto it = ancestry.rbegin(); it != ancestry.rend(); ++it) if (FFlag::LuauClipExtraHasEndProps)
{ {
if (AstStatForIn* statForIn = (*it)->as<AstStatForIn>(); statForIn && !statForIn->hasEnd) for (auto it = ancestry.rbegin(); it != ancestry.rend(); ++it)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstStatFor* statFor = (*it)->as<AstStatFor>(); statFor && !statFor->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstStatIf* statIf = (*it)->as<AstStatIf>(); statIf && !statIf->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstStatWhile* statWhile = (*it)->as<AstStatWhile>(); statWhile && !statWhile->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstExprFunction* exprFunction = (*it)->as<AstExprFunction>(); exprFunction && !exprFunction->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
if (FFlag::LuauAutocompleteDoEnd)
{ {
if (AstStatBlock* exprBlock = (*it)->as<AstStatBlock>(); exprBlock && !exprBlock->hasEnd) if (AstStatForIn* statForIn = (*it)->as<AstStatForIn>(); statForIn && !statForIn->body->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword}); result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstStatFor* statFor = (*it)->as<AstStatFor>(); statFor && !statFor->body->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstStatIf* statIf = (*it)->as<AstStatIf>())
{
bool hasEnd = statIf->thenbody->hasEnd;
if (statIf->elsebody)
{
if (AstStatBlock* elseBlock = statIf->elsebody->as<AstStatBlock>())
hasEnd = elseBlock->hasEnd;
}
if (!hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
}
else if (AstStatWhile* statWhile = (*it)->as<AstStatWhile>(); statWhile && !statWhile->body->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstExprFunction* exprFunction = (*it)->as<AstExprFunction>(); exprFunction && !exprFunction->body->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
if (FFlag::LuauAutocompleteDoEnd)
{
if (AstStatBlock* exprBlock = (*it)->as<AstStatBlock>(); exprBlock && !exprBlock->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
}
}
}
else
{
for (auto it = ancestry.rbegin(); it != ancestry.rend(); ++it)
{
if (AstStatForIn* statForIn = (*it)->as<AstStatForIn>(); statForIn && !statForIn->DEPRECATED_hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstStatFor* statFor = (*it)->as<AstStatFor>(); statFor && !statFor->DEPRECATED_hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstStatIf* statIf = (*it)->as<AstStatIf>(); statIf && !statIf->DEPRECATED_hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstStatWhile* statWhile = (*it)->as<AstStatWhile>(); statWhile && !statWhile->DEPRECATED_hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstExprFunction* exprFunction = (*it)->as<AstExprFunction>(); exprFunction && !exprFunction->DEPRECATED_hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
if (FFlag::LuauAutocompleteDoEnd)
{
if (AstStatBlock* exprBlock = (*it)->as<AstStatBlock>(); exprBlock && !exprBlock->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
}
} }
} }
@ -1086,8 +1121,16 @@ static AutocompleteEntryMap autocompleteStatement(
} }
} }
if (AstStatRepeat* statRepeat = parent->as<AstStatRepeat>(); statRepeat && !statRepeat->hasUntil) if (FFlag::LuauClipExtraHasEndProps)
result.emplace("until", AutocompleteEntry{AutocompleteEntryKind::Keyword}); {
if (AstStatRepeat* statRepeat = parent->as<AstStatRepeat>(); statRepeat && !statRepeat->body->hasEnd)
result.emplace("until", AutocompleteEntry{AutocompleteEntryKind::Keyword});
}
else
{
if (AstStatRepeat* statRepeat = parent->as<AstStatRepeat>(); statRepeat && !statRepeat->DEPRECATED_hasUntil)
result.emplace("until", AutocompleteEntry{AutocompleteEntryKind::Keyword});
}
} }
if (ancestry.size() >= 4) if (ancestry.size() >= 4)
@ -1101,8 +1144,16 @@ static AutocompleteEntryMap autocompleteStatement(
} }
} }
if (AstStatRepeat* statRepeat = extractStat<AstStatRepeat>(ancestry); statRepeat && !statRepeat->hasUntil) if (FFlag::LuauClipExtraHasEndProps)
result.emplace("until", AutocompleteEntry{AutocompleteEntryKind::Keyword}); {
if (AstStatRepeat* statRepeat = extractStat<AstStatRepeat>(ancestry); statRepeat && !statRepeat->body->hasEnd)
result.emplace("until", AutocompleteEntry{AutocompleteEntryKind::Keyword});
}
else
{
if (AstStatRepeat* statRepeat = extractStat<AstStatRepeat>(ancestry); statRepeat && !statRepeat->DEPRECATED_hasUntil)
result.emplace("until", AutocompleteEntry{AutocompleteEntryKind::Keyword});
}
return result; return result;
} }

View file

@ -175,6 +175,8 @@ void ConstraintGraphBuilder::visitModuleRoot(AstStatBlock* block)
visitBlockWithoutChildScope(scope, block); visitBlockWithoutChildScope(scope, block);
fillInInferredBindings(scope, block);
if (logger) if (logger)
logger->captureGenerationModule(module); logger->captureGenerationModule(module);
} }
@ -586,90 +588,41 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat)
ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* local) ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* local)
{ {
std::vector<TypeId> varTypes; std::vector<std::optional<TypeId>> varTypes;
varTypes.reserve(local->vars.size); varTypes.reserve(local->vars.size);
std::vector<TypeId> assignees;
assignees.reserve(local->vars.size);
// Used to name the first value type, even if it's not placed in varTypes, // Used to name the first value type, even if it's not placed in varTypes,
// for the purpose of synthetic name attribution. // for the purpose of synthetic name attribution.
std::optional<TypeId> firstValueType; std::optional<TypeId> firstValueType;
for (AstLocal* local : local->vars) for (AstLocal* local : local->vars)
{ {
TypeId ty = nullptr; TypeId assignee = arena->addType(BlockedType{});
assignees.push_back(assignee);
if (!firstValueType)
firstValueType = assignee;
if (local->annotation) if (local->annotation)
ty = resolveType(scope, local->annotation, /* inTypeArguments */ false);
varTypes.push_back(ty);
}
for (size_t i = 0; i < local->values.size; ++i)
{
AstExpr* value = local->values.data[i];
const bool hasAnnotation = i < local->vars.size && nullptr != local->vars.data[i]->annotation;
if (value->is<AstExprConstantNil>())
{ {
// HACK: we leave nil-initialized things floating under the TypeId annotationTy = resolveType(scope, local->annotation, /* inTypeArguments */ false);
// assumption that they will later be populated. varTypes.push_back(annotationTy);
//
// See the test TypeInfer/infer_locals_with_nil_value. Better flow
// awareness should make this obsolete.
if (i < varTypes.size() && !varTypes[i]) addConstraint(scope, local->location, SubtypeConstraint{assignee, annotationTy});
varTypes[i] = freshType(scope);
}
// Only function calls and vararg expressions can produce packs. All
// other expressions produce exactly one value.
else if (i != local->values.size - 1 || (!value->is<AstExprCall>() && !value->is<AstExprVarargs>()))
{
std::optional<TypeId> expectedType;
if (hasAnnotation)
expectedType = varTypes.at(i);
TypeId exprType = check(scope, value, expectedType, /*forceSingleton*/ false, /*generalize*/ true).ty;
if (i < varTypes.size())
{
if (varTypes[i])
addConstraint(scope, local->location, SubtypeConstraint{exprType, varTypes[i]});
else
varTypes[i] = exprType;
}
if (i == 0)
firstValueType = exprType;
} }
else else
{ varTypes.push_back(std::nullopt);
std::vector<std::optional<TypeId>> expectedTypes;
if (hasAnnotation)
expectedTypes.insert(begin(expectedTypes), begin(varTypes) + i, end(varTypes));
TypePackId exprPack = checkPack(scope, value, expectedTypes, /*generalize*/ true).tp; BreadcrumbId bc = dfg->getBreadcrumb(local);
scope->lvalueTypes[bc->def] = assignee;
if (i < local->vars.size)
{
TypePack packTypes = extendTypePack(*arena, builtinTypes, exprPack, varTypes.size() - i);
// fill out missing values in varTypes with values from exprPack
for (size_t j = i; j < varTypes.size(); ++j)
{
if (!varTypes[j])
{
if (j - i < packTypes.head.size())
varTypes[j] = packTypes.head[j - i];
else
varTypes[j] = arena->addType(BlockedType{});
}
}
std::vector<TypeId> tailValues{varTypes.begin() + i, varTypes.end()};
TypePackId tailPack = arena->addTypePack(std::move(tailValues));
addConstraint(scope, local->location, UnpackConstraint{tailPack, exprPack});
}
}
} }
TypePackId resultPack = checkPack(scope, local->values, varTypes).tp;
addConstraint(scope, local->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack});
if (local->vars.size == 1 && local->values.size == 1 && firstValueType && scope.get() == rootScope) if (local->vars.size == 1 && local->values.size == 1 && firstValueType && scope.get() == rootScope)
{ {
AstLocal* var = local->vars.data[0]; AstLocal* var = local->vars.data[0];
@ -691,16 +644,16 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l
AstLocal* l = local->vars.data[i]; AstLocal* l = local->vars.data[i];
Location location = l->location; Location location = l->location;
if (!varTypes[i]) std::optional<TypeId> annotation = varTypes[i];
varTypes[i] = freshType(scope);
scope->bindings[l] = Binding{varTypes[i], location};
// HACK: In the greedy solver, we say the type state of a variable is the type annotation itself, but
// the actual type state is the corresponding initializer expression (if it exists) or nil otherwise.
BreadcrumbId bc = dfg->getBreadcrumb(l); BreadcrumbId bc = dfg->getBreadcrumb(l);
scope->lvalueTypes[bc->def] = varTypes[i];
scope->rvalueRefinements[bc->def] = varTypes[i]; if (annotation)
scope->bindings[l] = Binding{*annotation, location};
else
{
scope->bindings[l] = Binding{builtinTypes->neverType, location};
inferredBindings.emplace_back(scope.get(), l, bc);
}
} }
if (local->values.size > 0) if (local->values.size > 0)
@ -712,30 +665,32 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l
if (!call) if (!call)
continue; continue;
if (auto maybeRequire = matchRequire(*call)) auto maybeRequire = matchRequire(*call);
if (!maybeRequire)
continue;
AstExpr* require = *maybeRequire;
auto moduleInfo = moduleResolver->resolveModuleInfo(module->name, *require);
if (!moduleInfo)
continue;
ModulePtr module = moduleResolver->getModule(moduleInfo->name);
if (!module)
continue;
const Name name{local->vars.data[i]->name.value};
scope->importedTypeBindings[name] = module->exportedTypeBindings;
scope->importedModules[name] = moduleInfo->name;
// Imported types of requires that transitively refer to current module have to be replaced with 'any'
for (const auto& [location, path] : requireCycles)
{ {
AstExpr* require = *maybeRequire; if (path.empty() || path.front() != moduleInfo->name)
continue;
if (auto moduleInfo = moduleResolver->resolveModuleInfo(module->name, *require)) for (auto& [name, tf] : scope->importedTypeBindings[name])
{ tf = TypeFun{{}, {}, builtinTypes->anyType};
const Name name{local->vars.data[i]->name.value};
if (ModulePtr module = moduleResolver->getModule(moduleInfo->name))
{
scope->importedTypeBindings[name] = module->exportedTypeBindings;
scope->importedModules[name] = moduleInfo->name;
// Imported types of requires that transitively refer to current module have to be replaced with 'any'
for (const auto& [location, path] : requireCycles)
{
if (!path.empty() && path.front() == moduleInfo->name)
{
for (auto& [name, tf] : scope->importedTypeBindings[name])
tf = TypeFun{{}, {}, builtinTypes->anyType};
}
}
}
}
} }
} }
} }
@ -781,6 +736,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* f
std::vector<TypeId> variableTypes; std::vector<TypeId> variableTypes;
variableTypes.reserve(forIn->vars.size); variableTypes.reserve(forIn->vars.size);
for (AstLocal* var : forIn->vars) for (AstLocal* var : forIn->vars)
{ {
TypeId ty = nullptr; TypeId ty = nullptr;
@ -790,18 +746,18 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* f
ty = freshType(loopScope); ty = freshType(loopScope);
loopScope->bindings[var] = Binding{ty, var->location}; loopScope->bindings[var] = Binding{ty, var->location};
variableTypes.push_back(ty);
TypeId assignee = arena->addType(BlockedType{});
variableTypes.push_back(assignee);
BreadcrumbId bc = dfg->getBreadcrumb(var); BreadcrumbId bc = dfg->getBreadcrumb(var);
loopScope->lvalueTypes[bc->def] = ty; loopScope->lvalueTypes[bc->def] = assignee;
loopScope->rvalueRefinements[bc->def] = ty;
} }
// It is always ok to provide too few variables, so we give this pack a free tail. TypePackId variablePack = arena->addTypePack(std::move(variableTypes));
TypePackId variablePack = arena->addTypePack(std::move(variableTypes), freshTypePack(loopScope));
addConstraint( addConstraint(
loopScope, getLocation(forIn->values), IterableConstraint{iterator, variablePack, forIn->values.data[0], &module->astForInNextTypes}); loopScope, getLocation(forIn->values), IterableConstraint{iterator, variablePack, forIn->values.data[0], &module->astForInNextTypes});
visit(loopScope, forIn->body); visit(loopScope, forIn->body);
return ControlFlow::None; return ControlFlow::None;
@ -1033,24 +989,30 @@ static void bindFreeType(TypeId a, TypeId b)
ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign) ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign)
{ {
std::vector<TypeId> varTypes = checkLValues(scope, assign->vars);
std::vector<std::optional<TypeId>> expectedTypes; std::vector<std::optional<TypeId>> expectedTypes;
expectedTypes.reserve(varTypes.size()); expectedTypes.reserve(assign->vars.size);
for (TypeId ty : varTypes) std::vector<TypeId> assignees;
assignees.reserve(assign->vars.size);
for (AstExpr* lvalue : assign->vars)
{ {
ty = follow(ty); TypeId upperBound = follow(checkLValue(scope, lvalue));
if (get<FreeType>(ty)) if (get<FreeType>(upperBound))
expectedTypes.push_back(std::nullopt); expectedTypes.push_back(std::nullopt);
else else
expectedTypes.push_back(ty); expectedTypes.push_back(upperBound);
TypeId assignee = arena->addType(BlockedType{});
assignees.push_back(assignee);
addConstraint(scope, lvalue->location, SubtypeConstraint{assignee, upperBound});
if (NullableBreadcrumbId bc = dfg->getBreadcrumb(lvalue))
scope->lvalueTypes[bc->def] = assignee;
} }
TypePackId exprPack = checkPack(scope, assign->values, expectedTypes).tp; TypePackId resultPack = checkPack(scope, assign->values, expectedTypes).tp;
TypePackId varPack = arena->addTypePack({varTypes}); addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack});
addConstraint(scope, assign->location, PackSubtypeConstraint{exprPack, varPack});
return ControlFlow::None; return ControlFlow::None;
} }
@ -1729,7 +1691,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* loc
if (auto ty = scope->lookup(bc->def)) if (auto ty = scope->lookup(bc->def))
return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)}; return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)};
else else
ice->ice("AstExprLocal came before its declaration?"); ice->ice("CGB: AstExprLocal came before its declaration?");
} }
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* global) Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* global)
@ -1837,13 +1799,26 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction*
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprUnary* unary) Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprUnary* unary)
{ {
auto [operandType, refinement] = check(scope, unary->expr); auto [operandType, refinement] = check(scope, unary->expr);
TypeId resultType = arena->addType(BlockedType{});
addConstraint(scope, unary->location, UnaryConstraint{unary->op, operandType, resultType});
if (unary->op == AstExprUnary::Not) switch (unary->op)
{
case AstExprUnary::Op::Not:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.notFamily},
{operandType},
{},
});
addConstraint(scope, unary->location, ReduceConstraint{resultType});
return Inference{resultType, refinementArena.negation(refinement)}; return Inference{resultType, refinementArena.negation(refinement)};
else }
default:
{
TypeId resultType = arena->addType(BlockedType{});
addConstraint(scope, unary->location, UnaryConstraint{unary->op, operandType, resultType});
return Inference{resultType}; return Inference{resultType};
}
}
} }
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* binary, std::optional<TypeId> expectedType) Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* binary, std::optional<TypeId> expectedType)
@ -1922,6 +1897,16 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* bi
addConstraint(scope, binary->location, ReduceConstraint{resultType}); addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)}; return Inference{resultType, std::move(refinement)};
} }
case AstExprBinary::Op::Concat:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.concatFamily},
{leftType, rightType},
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
case AstExprBinary::Op::And: case AstExprBinary::Op::And:
{ {
TypeId resultType = arena->addType(TypeFamilyInstanceType{ TypeId resultType = arena->addType(TypeFamilyInstanceType{
@ -1942,6 +1927,57 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* bi
addConstraint(scope, binary->location, ReduceConstraint{resultType}); addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)}; return Inference{resultType, std::move(refinement)};
} }
case AstExprBinary::Op::CompareLt:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.ltFamily},
{leftType, rightType},
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
case AstExprBinary::Op::CompareGe:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.ltFamily},
{rightType, leftType}, // lua decided that `__ge(a, b)` is instead just `__lt(b, a)`
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
case AstExprBinary::Op::CompareLe:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.leFamily},
{leftType, rightType},
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
case AstExprBinary::Op::CompareGt:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.leFamily},
{rightType, leftType}, // lua decided that `__gt(a, b)` is instead just `__le(b, a)`
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
case AstExprBinary::Op::CompareEq:
case AstExprBinary::Op::CompareNe:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
NotNull{&kBuiltinTypeFamilies.eqFamily},
{leftType, rightType},
{},
});
addConstraint(scope, binary->location, ReduceConstraint{resultType});
return Inference{resultType, std::move(refinement)};
}
default: default:
{ {
TypeId resultType = arena->addType(BlockedType{}); TypeId resultType = arena->addType(BlockedType{});
@ -2099,17 +2135,6 @@ std::tuple<TypeId, TypeId, RefinementId> ConstraintGraphBuilder::checkBinary(
} }
} }
std::vector<TypeId> ConstraintGraphBuilder::checkLValues(const ScopePtr& scope, AstArray<AstExpr*> exprs)
{
std::vector<TypeId> types;
types.reserve(exprs.size);
for (AstExpr* expr : exprs)
types.push_back(checkLValue(scope, expr));
return types;
}
TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr) TypeId ConstraintGraphBuilder::checkLValue(const ScopePtr& scope, AstExpr* expr)
{ {
if (auto local = expr->as<AstExprLocal>()) if (auto local = expr->as<AstExprLocal>())
@ -2201,6 +2226,7 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex
return check(scope, expr).ty; return check(scope, expr).ty;
Symbol sym; Symbol sym;
NullableBreadcrumbId bc = nullptr;
std::vector<std::string> segments; std::vector<std::string> segments;
std::vector<AstExpr*> exprs; std::vector<AstExpr*> exprs;
@ -2210,11 +2236,13 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex
if (auto global = e->as<AstExprGlobal>()) if (auto global = e->as<AstExprGlobal>())
{ {
sym = global->name; sym = global->name;
bc = dfg->getBreadcrumb(global);
break; break;
} }
else if (auto local = e->as<AstExprLocal>()) else if (auto local = e->as<AstExprLocal>())
{ {
sym = local->local; sym = local->local;
bc = dfg->getBreadcrumb(local);
break; break;
} }
else if (auto indexName = e->as<AstExprIndexName>()) else if (auto indexName = e->as<AstExprIndexName>())
@ -2251,7 +2279,16 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex
if (!lookupResult) if (!lookupResult)
return check(scope, expr).ty; return check(scope, expr).ty;
const auto [subjectBinding, symbolScope] = std::move(*lookupResult); const auto [subjectBinding, symbolScope] = std::move(*lookupResult);
TypeId subjectType = subjectBinding->typeId;
LUAU_ASSERT(bc);
std::optional<TypeId> subjectTy = scope->lookup(bc->def);
/* If we have a breadcrumb but no type, it can only mean that we're setting
* a property of some builtin table. This isn't legal, but we still want to
* wire up the constraints properly so that we can report why it is not
* legal.
*/
TypeId subjectType = subjectTy.value_or(subjectBinding->typeId);
TypeId propTy = freshType(scope); TypeId propTy = freshType(scope);
@ -2502,7 +2539,6 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
argTy = resolveType(signatureScope, local->annotation, /* inTypeArguments */ false, /* replaceErrorWithFresh*/ true); argTy = resolveType(signatureScope, local->annotation, /* inTypeArguments */ false, /* replaceErrorWithFresh*/ true);
else else
{ {
if (i < expectedArgPack.head.size()) if (i < expectedArgPack.head.size())
argTy = expectedArgPack.head[i]; argTy = expectedArgPack.head[i];
else else
@ -2511,7 +2547,15 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
argTypes.push_back(argTy); argTypes.push_back(argTy);
argNames.emplace_back(FunctionArgument{local->name.value, local->location}); argNames.emplace_back(FunctionArgument{local->name.value, local->location});
signatureScope->bindings[local] = Binding{argTy, local->location};
if (local->annotation)
signatureScope->bindings[local] = Binding{argTy, local->location};
else
{
BreadcrumbId bc = dfg->getBreadcrumb(local);
signatureScope->bindings[local] = Binding{builtinTypes->neverType, local->location};
inferredBindings.emplace_back(signatureScope.get(), local, bc);
}
BreadcrumbId bc = dfg->getBreadcrumb(local); BreadcrumbId bc = dfg->getBreadcrumb(local);
signatureScope->lvalueTypes[bc->def] = argTy; signatureScope->lvalueTypes[bc->def] = argTy;
@ -3018,6 +3062,37 @@ void ConstraintGraphBuilder::prepopulateGlobalScope(const ScopePtr& globalScope,
program->visit(&gp); program->visit(&gp);
} }
void ConstraintGraphBuilder::fillInInferredBindings(const ScopePtr& globalScope, AstStatBlock* block)
{
std::deque<BreadcrumbId> queue;
for (const auto& [scope, symbol, breadcrumb] : inferredBindings)
{
LUAU_ASSERT(queue.empty());
queue.push_back(breadcrumb);
TypeId ty = builtinTypes->neverType;
while (!queue.empty())
{
const BreadcrumbId bc = queue.front();
queue.pop_front();
TypeId* lvalueType = scope->lvalueTypes.find(bc->def);
if (!lvalueType)
continue;
ty = simplifyUnion(builtinTypes, arena, ty, *lvalueType).result;
for (BreadcrumbId child : bc->children)
queue.push_back(child);
}
scope->bindings[symbol].typeId = ty;
}
}
std::vector<std::optional<TypeId>> ConstraintGraphBuilder::getExpectedCallTypesForFunctionOverloads(const TypeId fnType) std::vector<std::optional<TypeId>> ConstraintGraphBuilder::getExpectedCallTypesForFunctionOverloads(const TypeId fnType)
{ {
std::vector<TypeId> funTys; std::vector<TypeId> funTys;

View file

@ -1785,13 +1785,13 @@ bool ConstraintSolver::tryDispatch(const UnpackConstraint& c, NotNull<const Cons
// We know that resultPack does not have a tail, but we don't know if // We know that resultPack does not have a tail, but we don't know if
// sourcePack is long enough to fill every value. Replace every remaining // sourcePack is long enough to fill every value. Replace every remaining
// result TypeId with the error recovery type. // result TypeId with `nil`.
while (destIter != destEnd) while (destIter != destEnd)
{ {
if (isBlocked(*destIter)) if (isBlocked(*destIter))
{ {
asMutable(*destIter)->ty.emplace<BoundType>(builtinTypes->errorRecoveryType()); asMutable(*destIter)->ty.emplace<BoundType>(builtinTypes->nilType);
unblock(*destIter, constraint->location); unblock(*destIter, constraint->location);
} }
@ -2013,57 +2013,26 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
if (get<FreeType>(iteratorTy)) if (get<FreeType>(iteratorTy))
return block_(iteratorTy); return block_(iteratorTy);
auto anyify = [&](auto ty) { auto unpack = [&](TypeId ty) {
Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, builtinTypes->anyType, builtinTypes->anyTypePack}; TypePackId variadic = arena->addTypePack(VariadicTypePack{ty});
std::optional anyified = anyify.substitute(ty); pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, variadic});
if (!anyified)
reportError(CodeTooComplex{}, constraint->location);
else
unify(constraint->scope, constraint->location, *anyified, ty);
};
auto unknownify = [&](auto ty) {
Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, builtinTypes->unknownType, builtinTypes->anyTypePack};
std::optional anyified = anyify.substitute(ty);
if (!anyified)
reportError(CodeTooComplex{}, constraint->location);
else
unify(constraint->scope, constraint->location, *anyified, ty);
};
auto errorify = [&](auto ty) {
Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, errorRecoveryType(), errorRecoveryTypePack()};
std::optional errorified = anyify.substitute(ty);
if (!errorified)
reportError(CodeTooComplex{}, constraint->location);
else
unify(constraint->scope, constraint->location, *errorified, ty);
};
auto neverify = [&](auto ty) {
Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, builtinTypes->neverType, builtinTypes->neverTypePack};
std::optional neverified = anyify.substitute(ty);
if (!neverified)
reportError(CodeTooComplex{}, constraint->location);
else
unify(constraint->scope, constraint->location, *neverified, ty);
}; };
if (get<AnyType>(iteratorTy)) if (get<AnyType>(iteratorTy))
{ {
anyify(c.variables); unpack(builtinTypes->anyType);
return true; return true;
} }
if (get<ErrorType>(iteratorTy)) if (get<ErrorType>(iteratorTy))
{ {
errorify(c.variables); unpack(builtinTypes->errorType);
return true; return true;
} }
if (get<NeverType>(iteratorTy)) if (get<NeverType>(iteratorTy))
{ {
neverify(c.variables); unpack(builtinTypes->neverType);
return true; return true;
} }
@ -2088,7 +2057,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
unify(constraint->scope, constraint->location, c.variables, expectedVariablePack); unify(constraint->scope, constraint->location, c.variables, expectedVariablePack);
} }
else else
errorify(c.variables); unpack(builtinTypes->errorType);
} }
else if (std::optional<TypeId> iterFn = findMetatableEntry(builtinTypes, errors, iteratorTy, "__iter", Location{})) else if (std::optional<TypeId> iterFn = findMetatableEntry(builtinTypes, errors, iteratorTy, "__iter", Location{}))
{ {
@ -2128,7 +2097,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
const TypeId expectedNextTy = arena->addType(FunctionType{nextArgPack, nextRetPack}); const TypeId expectedNextTy = arena->addType(FunctionType{nextArgPack, nextRetPack});
unify(constraint->scope, constraint->location, *instantiatedNextFn, expectedNextTy); unify(constraint->scope, constraint->location, *instantiatedNextFn, expectedNextTy);
pushConstraint(constraint->scope, constraint->location, PackSubtypeConstraint{c.variables, nextRetPack}); pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, nextRetPack});
} }
else else
{ {
@ -2154,9 +2123,9 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
LUAU_ASSERT(false); LUAU_ASSERT(false);
} }
else if (auto primitiveTy = get<PrimitiveType>(iteratorTy); primitiveTy && primitiveTy->type == PrimitiveType::Type::Table) else if (auto primitiveTy = get<PrimitiveType>(iteratorTy); primitiveTy && primitiveTy->type == PrimitiveType::Type::Table)
unknownify(c.variables); unpack(builtinTypes->unknownType);
else else
errorify(c.variables); unpack(builtinTypes->errorType);
return true; return true;
} }

View file

@ -1,10 +1,13 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/DataFlowGraph.h" #include "Luau/DataFlowGraph.h"
#include "Luau/Ast.h"
#include "Luau/Breadcrumb.h" #include "Luau/Breadcrumb.h"
#include "Luau/Error.h" #include "Luau/Error.h"
#include "Luau/Refinement.h" #include "Luau/Refinement.h"
#include <algorithm>
LUAU_FASTFLAG(DebugLuauFreezeArena) LUAU_FASTFLAG(DebugLuauFreezeArena)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
@ -278,11 +281,12 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f)
void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a) void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a)
{ {
for (AstExpr* r : a->values) for (size_t i = 0; i < std::max(a->vars.size, a->values.size); ++i)
visitExpr(scope, r); {
BreadcrumbId bc = i < a->values.size ? visitExpr(scope, a->values.data[i]) : breadcrumbs->add(nullptr, defs->freshCell());
for (AstExpr* l : a->vars) if (i < a->vars.size)
visitLValue(scope, l); visitLValue(scope, a->vars.data[i], bc);
}
} }
void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c) void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c)
@ -291,12 +295,11 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatCompoundAssign* c)
// but the `c->var` only has one pointer address, so we need to come up with a way to store both. // but the `c->var` only has one pointer address, so we need to come up with a way to store both.
// For now, it's not important because we don't have type states, but it is going to be important, e.g. // For now, it's not important because we don't have type states, but it is going to be important, e.g.
// //
// local a = 5 -- a[1] // local a = 5 -- a-1
// a += 5 -- a[2] = a[1] + 5 // a += 5 -- a-2 = a-1 + 5
// //
// We can't just visit `c->var` as a rvalue and then separately traverse `c->var` as an lvalue, since that's O(n^2). // We can't just visit `c->var` as a rvalue and then separately traverse `c->var` as an lvalue, since that's O(n^2).
visitLValue(scope, c->var); visitLValue(scope, c->var, visitExpr(scope, c->value));
visitExpr(scope, c->value);
} }
void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f) void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f)
@ -311,17 +314,14 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f)
// //
// which is evidence that references to variables must be a phi node of all possible definitions, // which is evidence that references to variables must be a phi node of all possible definitions,
// but for bug compatibility, we'll assume the same thing here. // but for bug compatibility, we'll assume the same thing here.
visitLValue(scope, f->name); visitLValue(scope, f->name, visitExpr(scope, f->func));
visitExpr(scope, f->func);
} }
void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocalFunction* l) void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocalFunction* l)
{ {
BreadcrumbId bc = breadcrumbs->add(nullptr, defs->freshCell()); BreadcrumbId bc = visitExpr(scope, l->func);
graph.localBreadcrumbs[l->name] = bc; graph.localBreadcrumbs[l->name] = bc;
scope->bindings[l->name] = bc; scope->bindings[l->name] = bc;
visitExpr(scope, l->func);
} }
void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatTypeAlias* t) void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatTypeAlias* t)
@ -423,7 +423,7 @@ BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l)
{ {
NullableBreadcrumbId breadcrumb = scope->lookup(l->local); NullableBreadcrumbId breadcrumb = scope->lookup(l->local);
if (!breadcrumb) if (!breadcrumb)
handle->ice("AstExprLocal came before its declaration?"); handle->ice("DFG: AstExprLocal came before its declaration?");
graph.astBreadcrumbs[l] = breadcrumb; graph.astBreadcrumbs[l] = breadcrumb;
return NotNull{breadcrumb}; return NotNull{breadcrumb};
@ -591,81 +591,69 @@ BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprError* erro
return breadcrumbs->add(nullptr, defs->freshCell()); return breadcrumbs->add(nullptr, defs->freshCell());
} }
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e) void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExpr* e, BreadcrumbId bc)
{ {
if (auto l = e->as<AstExprLocal>()) if (auto l = e->as<AstExprLocal>())
return visitLValue(scope, l); return visitLValue(scope, l, bc);
else if (auto g = e->as<AstExprGlobal>()) else if (auto g = e->as<AstExprGlobal>())
return visitLValue(scope, g); return visitLValue(scope, g, bc);
else if (auto i = e->as<AstExprIndexName>()) else if (auto i = e->as<AstExprIndexName>())
return visitLValue(scope, i); return visitLValue(scope, i, bc);
else if (auto i = e->as<AstExprIndexExpr>()) else if (auto i = e->as<AstExprIndexExpr>())
return visitLValue(scope, i); return visitLValue(scope, i, bc);
else if (auto error = e->as<AstExprError>()) else if (auto error = e->as<AstExprError>())
{ return visitLValue(scope, error, bc);
visitExpr(scope, error); // TODO: is this right?
return;
}
else else
handle->ice("Unknown AstExpr in DataFlowGraphBuilder::visitLValue"); handle->ice("Unknown AstExpr in DataFlowGraphBuilder::visitLValue");
} }
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l) void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprLocal* l, BreadcrumbId bc)
{ {
// Bug compatibility: we don't support type states yet, so we need to do this. // In order to avoid alias tracking, we need to clip the reference to the parent breadcrumb
NullableBreadcrumbId bc = scope->lookup(l->local); // as well as the def that was about to be assigned onto this lvalue. However, we want to
LUAU_ASSERT(bc); // copy the metadata so that refinements can be consistent.
BreadcrumbId updated = breadcrumbs->add(scope->lookup(l->local), defs->freshCell(), bc->metadata);
graph.astBreadcrumbs[l] = bc; graph.astBreadcrumbs[l] = updated;
scope->bindings[l->local] = bc; scope->bindings[l->local] = updated;
} }
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g) void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprGlobal* g, BreadcrumbId bc)
{ {
// Bug compatibility: we don't support type states yet, so we need to do this. // In order to avoid alias tracking, we need to clip the reference to the parent breadcrumb
NullableBreadcrumbId bc = scope->lookup(g->name); // as well as the def that was about to be assigned onto this lvalue. However, we want to
if (!bc) // copy the metadata so that refinements can be consistent.
bc = breadcrumbs->add(nullptr, defs->freshCell()); BreadcrumbId updated = breadcrumbs->add(scope->lookup(g->name), defs->freshCell(), bc->metadata);
graph.astBreadcrumbs[g] = updated;
graph.astBreadcrumbs[g] = bc; scope->bindings[g->name] = updated;
scope->bindings[g->name] = bc;
} }
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexName* i) void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexName* i, BreadcrumbId bc)
{ {
// Bug compatibility: we don't support type states yet, so we need to do this.
BreadcrumbId parentBreadcrumb = visitExpr(scope, i->expr); BreadcrumbId parentBreadcrumb = visitExpr(scope, i->expr);
std::string key = i->index.value; BreadcrumbId updated = breadcrumbs->add(scope->props[parentBreadcrumb->def][i->index.value], defs->freshCell(), bc->metadata);
NullableBreadcrumbId propBreadcrumb = scope->lookup(parentBreadcrumb->def, key); graph.astBreadcrumbs[i] = updated;
if (!propBreadcrumb) scope->props[parentBreadcrumb->def][i->index.value] = updated;
{
propBreadcrumb = breadcrumbs->emplace<FieldMetadata>(parentBreadcrumb, defs->freshCell(), key);
moduleScope->props[parentBreadcrumb->def][key] = propBreadcrumb;
}
graph.astBreadcrumbs[i] = propBreadcrumb;
} }
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i) void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprIndexExpr* i, BreadcrumbId bc)
{ {
BreadcrumbId parentBreadcrumb = visitExpr(scope, i->expr); BreadcrumbId parentBreadcrumb = visitExpr(scope, i->expr);
visitExpr(scope, i->index); visitExpr(scope, i->index);
if (auto string = i->index->as<AstExprConstantString>()) if (auto string = i->index->as<AstExprConstantString>())
{ {
std::string key{string->value.data, string->value.size}; BreadcrumbId updated = breadcrumbs->add(scope->props[parentBreadcrumb->def][string->value.data], defs->freshCell(), bc->metadata);
NullableBreadcrumbId propBreadcrumb = scope->lookup(parentBreadcrumb->def, key); graph.astBreadcrumbs[i] = updated;
if (!propBreadcrumb) scope->props[parentBreadcrumb->def][string->value.data] = updated;
{
propBreadcrumb = breadcrumbs->add(parentBreadcrumb, parentBreadcrumb->def);
moduleScope->props[parentBreadcrumb->def][key] = propBreadcrumb;
}
graph.astBreadcrumbs[i] = propBreadcrumb;
} }
} }
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprError* error, BreadcrumbId bc)
{
visitExpr(scope, error);
}
void DataFlowGraphBuilder::visitType(DfgScope* scope, AstType* t) void DataFlowGraphBuilder::visitType(DfgScope* scope, AstType* t)
{ {
if (auto r = t->as<AstTypeReference>()) if (auto r = t->as<AstTypeReference>())

View file

@ -112,7 +112,7 @@ struct ErrorConverter
result += "\ncaused by:\n "; result += "\ncaused by:\n ";
if (!tm.reason.empty()) if (!tm.reason.empty())
result += tm.reason + " \n"; result += tm.reason + "\n";
result += Luau::toString(*tm.error, TypeErrorToStringOptions{fileResolver}); result += Luau::toString(*tm.error, TypeErrorToStringOptions{fileResolver});
} }
@ -521,6 +521,13 @@ struct ErrorConverter
" depends on generic function parameters but does not appear in the function signature; this construct cannot be type-checked at this " " depends on generic function parameters but does not appear in the function signature; this construct cannot be type-checked at this "
"time"; "time";
} }
std::string operator()(const CheckedFunctionCallError& e) const
{
// TODO: What happens if checkedFunctionName cannot be found??
return "Function '" + e.checkedFunctionName + "' expects '" + toString(e.expected) + "' at argument #" + std::to_string(e.argumentIndex) +
", but got '" + Luau::toString(e.passed) + "'";
}
}; };
struct InvalidNameChecker struct InvalidNameChecker
@ -843,6 +850,12 @@ bool PackWhereClauseNeeded::operator==(const PackWhereClauseNeeded& rhs) const
return tp == rhs.tp; return tp == rhs.tp;
} }
bool CheckedFunctionCallError::operator==(const CheckedFunctionCallError& rhs) const
{
return *expected == *rhs.expected && *passed == *rhs.passed && checkedFunctionName == rhs.checkedFunctionName &&
argumentIndex == rhs.argumentIndex;
}
std::string toString(const TypeError& error) std::string toString(const TypeError& error)
{ {
return toString(error, TypeErrorToStringOptions{}); return toString(error, TypeErrorToStringOptions{});
@ -1009,6 +1022,11 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState)
e.ty = clone(e.ty); e.ty = clone(e.ty);
else if constexpr (std::is_same_v<T, PackWhereClauseNeeded>) else if constexpr (std::is_same_v<T, PackWhereClauseNeeded>)
e.tp = clone(e.tp); e.tp = clone(e.tp);
else if constexpr (std::is_same_v<T, CheckedFunctionCallError>)
{
e.expected = clone(e.expected);
e.passed = clone(e.passed);
}
else else
static_assert(always_false_v<T>, "Non-exhaustive type switch"); static_assert(always_false_v<T>, "Non-exhaustive type switch");
} }

View file

@ -15,6 +15,7 @@
#include "Luau/StringUtils.h" #include "Luau/StringUtils.h"
#include "Luau/TimeTrace.h" #include "Luau/TimeTrace.h"
#include "Luau/TypeChecker2.h" #include "Luau/TypeChecker2.h"
#include "Luau/NonStrictTypeChecker.h"
#include "Luau/TypeInfer.h" #include "Luau/TypeInfer.h"
#include "Luau/Variant.h" #include "Luau/Variant.h"
@ -37,7 +38,6 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false)
LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false) LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false)
LUAU_FASTFLAGVARIABLE(LuauTypecheckLimitControls, false) LUAU_FASTFLAGVARIABLE(LuauTypecheckLimitControls, false)
LUAU_FASTFLAGVARIABLE(CorrectEarlyReturnInMarkDirty, false) LUAU_FASTFLAGVARIABLE(CorrectEarlyReturnInMarkDirty, false)
LUAU_FASTFLAGVARIABLE(DebugLuauNewNonStrictMode, false)
namespace Luau namespace Luau
{ {
@ -1212,17 +1212,17 @@ const SourceModule* Frontend::getSourceModule(const ModuleName& moduleName) cons
return const_cast<Frontend*>(this)->getSourceModule(moduleName); return const_cast<Frontend*>(this)->getSourceModule(moduleName);
} }
ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle>& requireCycles, NotNull<BuiltinTypes> builtinTypes, ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vector<RequireCycle>& requireCycles, NotNull<BuiltinTypes> builtinTypes,
NotNull<InternalErrorReporter> iceHandler, NotNull<ModuleResolver> moduleResolver, NotNull<FileResolver> fileResolver, NotNull<InternalErrorReporter> iceHandler, NotNull<ModuleResolver> moduleResolver, NotNull<FileResolver> fileResolver,
const ScopePtr& parentScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options, const ScopePtr& parentScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options,
TypeCheckLimits limits) TypeCheckLimits limits)
{ {
const bool recordJsonLog = FFlag::DebugLuauLogSolverToJson; const bool recordJsonLog = FFlag::DebugLuauLogSolverToJson;
return check(sourceModule, requireCycles, builtinTypes, iceHandler, moduleResolver, fileResolver, parentScope, std::move(prepareModuleScope), return check(sourceModule, mode, requireCycles, builtinTypes, iceHandler, moduleResolver, fileResolver, parentScope,
options, limits, recordJsonLog); std::move(prepareModuleScope), options, limits, recordJsonLog);
} }
ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle>& requireCycles, NotNull<BuiltinTypes> builtinTypes, ModulePtr check(const SourceModule& sourceModule, Mode mode, const std::vector<RequireCycle>& requireCycles, NotNull<BuiltinTypes> builtinTypes,
NotNull<InternalErrorReporter> iceHandler, NotNull<ModuleResolver> moduleResolver, NotNull<FileResolver> fileResolver, NotNull<InternalErrorReporter> iceHandler, NotNull<ModuleResolver> moduleResolver, NotNull<FileResolver> fileResolver,
const ScopePtr& parentScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options, const ScopePtr& parentScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options,
TypeCheckLimits limits, bool recordJsonLog) TypeCheckLimits limits, bool recordJsonLog)
@ -1303,7 +1303,10 @@ ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle
} }
else else
{ {
Luau::check(builtinTypes, NotNull{&unifierState}, NotNull{&limits}, logger.get(), sourceModule, result.get()); if (mode == Mode::Nonstrict)
Luau::checkNonStrict(builtinTypes, iceHandler, NotNull{&unifierState}, NotNull{&dfg}, NotNull{&limits}, sourceModule, result.get());
else
Luau::check(builtinTypes, NotNull{&unifierState}, NotNull{&limits}, logger.get(), sourceModule, result.get());
} }
// It would be nice if we could freeze the arenas before doing type // It would be nice if we could freeze the arenas before doing type
@ -1332,7 +1335,7 @@ ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle
ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, std::vector<RequireCycle> requireCycles, ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, std::vector<RequireCycle> requireCycles,
std::optional<ScopePtr> environmentScope, bool forAutocomplete, bool recordJsonLog, TypeCheckLimits typeCheckLimits) std::optional<ScopePtr> environmentScope, bool forAutocomplete, bool recordJsonLog, TypeCheckLimits typeCheckLimits)
{ {
if (FFlag::DebugLuauDeferredConstraintResolution && mode == Mode::Strict) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
auto prepareModuleScopeWrap = [this, forAutocomplete](const ModuleName& name, const ScopePtr& scope) { auto prepareModuleScopeWrap = [this, forAutocomplete](const ModuleName& name, const ScopePtr& scope) {
if (prepareModuleScope) if (prepareModuleScope)
@ -1341,7 +1344,7 @@ ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, std::vect
try try
{ {
return Luau::check(sourceModule, requireCycles, builtinTypes, NotNull{&iceHandler}, return Luau::check(sourceModule, mode, requireCycles, builtinTypes, NotNull{&iceHandler},
NotNull{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver}, NotNull{fileResolver}, NotNull{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver}, NotNull{fileResolver},
environmentScope ? *environmentScope : globals.globalScope, prepareModuleScopeWrap, options, typeCheckLimits, recordJsonLog); environmentScope ? *environmentScope : globals.globalScope, prepareModuleScopeWrap, options, typeCheckLimits, recordJsonLog);
} }

View file

@ -200,6 +200,9 @@ static void errorToString(std::ostream& stream, const T& err)
stream << "WhereClauseNeeded { " << toString(err.ty) << " }"; stream << "WhereClauseNeeded { " << toString(err.ty) << " }";
else if constexpr (std::is_same_v<T, PackWhereClauseNeeded>) else if constexpr (std::is_same_v<T, PackWhereClauseNeeded>)
stream << "PackWhereClauseNeeded { " << toString(err.tp) << " }"; stream << "PackWhereClauseNeeded { " << toString(err.tp) << " }";
else if constexpr (std::is_same_v<T, CheckedFunctionCallError>)
stream << "CheckedFunctionCallError { expected = '" << toString(err.expected) << "', passed = '" << toString(err.passed)
<< "', checkedFunctionName = " << err.checkedFunctionName << ", argumentIndex = " << std::to_string(err.argumentIndex) << " }";
else else
static_assert(always_false_v<T>, "Non-exhaustive type switch"); static_assert(always_false_v<T>, "Non-exhaustive type switch");
} }

View file

@ -14,6 +14,9 @@
LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4) LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4)
LUAU_FASTFLAGVARIABLE(LuauLintDeprecatedFenv, false)
LUAU_FASTFLAGVARIABLE(LuauLintTableIndexer, false)
namespace Luau namespace Luau
{ {
@ -2085,6 +2088,32 @@ private:
return true; return true;
} }
bool visit(AstExprCall* node) override
{
// getfenv/setfenv are deprecated, however they are still used in some test frameworks and don't have a great general replacement
// for now we warn about the deprecation only when they are used with a numeric first argument; this produces fewer warnings and makes use
// of getfenv/setfenv a little more localized
if (FFlag::LuauLintDeprecatedFenv && !node->self && node->args.size >= 1)
{
if (AstExprGlobal* fenv = node->func->as<AstExprGlobal>(); fenv && (fenv->name == "getfenv" || fenv->name == "setfenv"))
{
AstExpr* level = node->args.data[0];
std::optional<TypeId> ty = context->getType(level);
if ((ty && isNumber(*ty)) || level->is<AstExprConstantNumber>())
{
// some common uses of getfenv(n) can be replaced by debug.info if the goal is to get the caller's identity
const char* suggestion = (fenv->name == "getfenv") ? "; consider using 'debug.info' instead" : "";
emitWarning(
*context, LintWarning::Code_DeprecatedApi, node->location, "Function '%s' is deprecated%s", fenv->name.value, suggestion);
}
}
}
return true;
}
void check(AstExprIndexName* node, TypeId ty) void check(AstExprIndexName* node, TypeId ty)
{ {
if (const ClassType* cty = get<ClassType>(ty)) if (const ClassType* cty = get<ClassType>(ty))
@ -2154,16 +2183,50 @@ private:
{ {
} }
bool visit(AstExprUnary* node) override
{
if (FFlag::LuauLintTableIndexer && node->op == AstExprUnary::Len)
checkIndexer(node, node->expr, "#");
return true;
}
bool visit(AstExprCall* node) override bool visit(AstExprCall* node) override
{ {
AstExprIndexName* func = node->func->as<AstExprIndexName>(); if (AstExprGlobal* func = node->func->as<AstExprGlobal>())
if (!func) {
return true; if (FFlag::LuauLintTableIndexer && func->name == "ipairs" && node->args.size == 1)
checkIndexer(node, node->args.data[0], "ipairs");
}
else if (AstExprIndexName* func = node->func->as<AstExprIndexName>())
{
if (AstExprGlobal* tablib = func->expr->as<AstExprGlobal>(); tablib && tablib->name == "table")
checkTableCall(node, func);
}
AstExprGlobal* tablib = func->expr->as<AstExprGlobal>(); return true;
if (!tablib || tablib->name != "table") }
return true;
void checkIndexer(AstExpr* node, AstExpr* expr, const char* op)
{
LUAU_ASSERT(FFlag::LuauLintTableIndexer);
std::optional<Luau::TypeId> ty = context->getType(expr);
if (!ty)
return;
const TableType* tty = get<TableType>(follow(*ty));
if (!tty)
return;
if (!tty->indexer && !tty->props.empty() && tty->state != TableState::Generic)
emitWarning(*context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table without an array part is likely a bug", op);
else if (tty->indexer && isString(tty->indexer->indexType)) // note: to avoid complexity of subtype tests we just check if the key is a string
emitWarning(*context, LintWarning::Code_TableOperations, node->location, "Using '%s' on a table with string keys is likely a bug", op);
}
void checkTableCall(AstExprCall* node, AstExprIndexName* func)
{
AstExpr** args = node->args.data; AstExpr** args = node->args.data;
if (func->index == "insert" && node->args.size == 2) if (func->index == "insert" && node->args.size == 2)
@ -2245,8 +2308,6 @@ private:
emitWarning(*context, LintWarning::Code_TableOperations, as->expr->location, emitWarning(*context, LintWarning::Code_TableOperations, as->expr->location,
"table.create with a table literal will reuse the same object for all elements; consider using a for loop instead"); "table.create with a table literal will reuse the same object for all elements; consider using a for loop instead");
} }
return true;
} }
bool isConstant(AstExpr* expr, double value) bool isConstant(AstExpr* expr, double value)

View file

@ -1,19 +1,60 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details // This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/NonStrictTypeChecker.h" #include "Luau/NonStrictTypeChecker.h"
#include "Luau/Ast.h"
#include "Luau/Common.h"
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/Subtyping.h" #include "Luau/Subtyping.h"
#include "Luau/Normalize.h" #include "Luau/Normalize.h"
#include "Luau/Error.h" #include "Luau/Error.h"
#include "Luau/TypeArena.h" #include "Luau/TypeArena.h"
#include "Luau/TypeFamily.h"
#include "Luau/Def.h" #include "Luau/Def.h"
#include <iostream>
namespace Luau namespace Luau
{ {
/* Push a scope onto the end of a stack for the lifetime of the StackPusher instance.
* NonStrictTypeChecker uses this to maintain knowledge about which scope encloses every
* given AstNode.
*/
struct StackPusher
{
std::vector<NotNull<Scope>>* stack;
NotNull<Scope> scope;
explicit StackPusher(std::vector<NotNull<Scope>>& stack, Scope* scope)
: stack(&stack)
, scope(scope)
{
stack.push_back(NotNull{scope});
}
~StackPusher()
{
if (stack)
{
LUAU_ASSERT(stack->back() == scope);
stack->pop_back();
}
}
StackPusher(const StackPusher&) = delete;
StackPusher&& operator=(const StackPusher&) = delete;
StackPusher(StackPusher&& other)
: stack(std::exchange(other.stack, nullptr))
, scope(other.scope)
{
}
};
struct NonStrictContext struct NonStrictContext
{ {
std::unordered_map<DefId, TypeId> context; std::unordered_map<const Def*, TypeId> context;
NonStrictContext() = default; NonStrictContext() = default;
@ -38,24 +79,14 @@ struct NonStrictContext
// TODO: unimplemented // TODO: unimplemented
} }
std::optional<TypeId> find(const DefId& def) std::optional<TypeId> find(const DefId& def) const
{ {
// TODO: unimplemented const Def* d = def.get();
auto it = context.find(d);
if (it != context.end())
return {it->second};
return {}; return {};
} }
// Satisfies means that for a given DefId n, and an actual type t for `n`, t satisfies the context if t <: context[n]
// ice if the DefId is not in the context
bool satisfies(const DefId& def, TypeId inferredType)
{
// TODO: unimplemented
return false;
}
bool willRunTimeError(const DefId& def, TypeId inferredType)
{
return satisfies(def, inferredType);
}
}; };
struct NonStrictTypeChecker struct NonStrictTypeChecker
@ -67,21 +98,341 @@ struct NonStrictTypeChecker
Module* module; Module* module;
Normalizer normalizer; Normalizer normalizer;
Subtyping subtyping; Subtyping subtyping;
NotNull<const DataFlowGraph> dfg;
DenseHashSet<TypeId> noTypeFamilyErrors{nullptr};
std::vector<NotNull<Scope>> stack;
const NotNull<TypeCheckLimits> limits;
NonStrictTypeChecker(NotNull<BuiltinTypes> builtinTypes, Subtyping subtyping, const NotNull<InternalErrorReporter> ice, NonStrictTypeChecker(NotNull<BuiltinTypes> builtinTypes, const NotNull<InternalErrorReporter> ice, NotNull<UnifierSharedState> unifierState,
NotNull<UnifierSharedState> unifierState, Module* module) NotNull<const DataFlowGraph> dfg, NotNull<TypeCheckLimits> limits, Module* module)
: builtinTypes(builtinTypes) : builtinTypes(builtinTypes)
, ice(ice) , ice(ice)
, module(module) , module(module)
, normalizer{&arena, builtinTypes, unifierState, /* cache inhabitance */ true} , normalizer{&arena, builtinTypes, unifierState, /* cache inhabitance */ true}
, subtyping{builtinTypes, NotNull{&arena}, NotNull(&normalizer), ice, NotNull{module->getModuleScope().get()}} , subtyping{builtinTypes, NotNull{&arena}, NotNull(&normalizer), ice, NotNull{module->getModuleScope().get()}}
, dfg(dfg)
, limits(limits)
{ {
} }
std::optional<StackPusher> pushStack(AstNode* node)
{
if (Scope** scope = module->astScopes.find(node))
return StackPusher{stack, *scope};
else
return std::nullopt;
}
TypeId flattenPack(TypePackId pack)
{
pack = follow(pack);
if (auto fst = first(pack, /*ignoreHiddenVariadics*/ false))
return *fst;
else if (auto ftp = get<FreeTypePack>(pack))
{
TypeId result = arena.addType(FreeType{ftp->scope});
TypePackId freeTail = arena.addTypePack(FreeTypePack{ftp->scope});
TypePack& resultPack = asMutable(pack)->ty.emplace<TypePack>();
resultPack.head.assign(1, result);
resultPack.tail = freeTail;
return result;
}
else if (get<Unifiable::Error>(pack))
return builtinTypes->errorRecoveryType();
else if (finite(pack) && size(pack) == 0)
return builtinTypes->nilType; // `(f())` where `f()` returns no values is coerced into `nil`
else
ice->ice("flattenPack got a weird pack!");
}
TypeId checkForFamilyInhabitance(TypeId instance, Location location)
{
if (noTypeFamilyErrors.find(instance))
return instance;
ErrorVec errors = reduceFamilies(
instance, location, TypeFamilyContext{NotNull{&arena}, builtinTypes, stack.back(), NotNull{&normalizer}, ice, limits}, true)
.errors;
if (errors.empty())
noTypeFamilyErrors.insert(instance);
// TODO??
// if (!isErrorSuppressing(location, instance))
// reportErrors(std::move(errors));
return instance;
}
TypeId lookupType(AstExpr* expr)
{
TypeId* ty = module->astTypes.find(expr);
if (ty)
return checkForFamilyInhabitance(follow(*ty), expr->location);
TypePackId* tp = module->astTypePacks.find(expr);
if (tp)
return checkForFamilyInhabitance(flattenPack(*tp), expr->location);
return builtinTypes->anyType;
}
void visit(AstStat* stat)
{
NonStrictContext fresh{};
visit(stat, fresh);
}
void visit(AstStat* stat, NonStrictContext& context)
{
auto pusher = pushStack(stat);
if (auto s = stat->as<AstStatBlock>())
return visit(s, context);
else if (auto s = stat->as<AstStatIf>())
return visit(s, context);
else if (auto s = stat->as<AstStatWhile>())
return visit(s, context);
else if (auto s = stat->as<AstStatRepeat>())
return visit(s, context);
else if (auto s = stat->as<AstStatBreak>())
return visit(s, context);
else if (auto s = stat->as<AstStatContinue>())
return visit(s, context);
else if (auto s = stat->as<AstStatReturn>())
return visit(s, context);
else if (auto s = stat->as<AstStatExpr>())
return visit(s, context);
else if (auto s = stat->as<AstStatLocal>())
return visit(s, context);
else if (auto s = stat->as<AstStatFor>())
return visit(s, context);
else if (auto s = stat->as<AstStatForIn>())
return visit(s, context);
else if (auto s = stat->as<AstStatAssign>())
return visit(s, context);
else if (auto s = stat->as<AstStatCompoundAssign>())
return visit(s, context);
else if (auto s = stat->as<AstStatFunction>())
return visit(s, context);
else if (auto s = stat->as<AstStatLocalFunction>())
return visit(s, context);
else if (auto s = stat->as<AstStatTypeAlias>())
return visit(s, context);
else if (auto s = stat->as<AstStatDeclareFunction>())
return visit(s, context);
else if (auto s = stat->as<AstStatDeclareGlobal>())
return visit(s, context);
else if (auto s = stat->as<AstStatDeclareClass>())
return visit(s, context);
else if (auto s = stat->as<AstStatError>())
return visit(s, context);
else
LUAU_ASSERT(!"NonStrictTypeChecker encountered an unknown node type");
}
void visit(AstStatBlock* block, NonStrictContext& context)
{
auto StackPusher = pushStack(block);
for (AstStat* statement : block->body)
visit(statement, context);
}
void visit(AstStatIf* ifStatement, NonStrictContext& context) {}
void visit(AstStatWhile* whileStatement, NonStrictContext& context) {}
void visit(AstStatRepeat* repeatStatement, NonStrictContext& context) {}
void visit(AstStatBreak* breakStatement, NonStrictContext& context) {}
void visit(AstStatContinue* continueStatement, NonStrictContext& context) {}
void visit(AstStatReturn* returnStatement, NonStrictContext& context) {}
void visit(AstStatExpr* expr, NonStrictContext& context)
{
visit(expr->expr, context);
}
void visit(AstStatLocal* local, NonStrictContext& context) {}
void visit(AstStatFor* forStatement, NonStrictContext& context) {}
void visit(AstStatForIn* forInStatement, NonStrictContext& context) {}
void visit(AstStatAssign* assign, NonStrictContext& context) {}
void visit(AstStatCompoundAssign* compoundAssign, NonStrictContext& context) {}
void visit(AstStatFunction* statFn, NonStrictContext& context) {}
void visit(AstStatLocalFunction* localFn, NonStrictContext& context) {}
void visit(AstStatTypeAlias* typeAlias, NonStrictContext& context) {}
void visit(AstStatDeclareFunction* declFn, NonStrictContext& context) {}
void visit(AstStatDeclareGlobal* declGlobal, NonStrictContext& context) {}
void visit(AstStatDeclareClass* declClass, NonStrictContext& context) {}
void visit(AstStatError* error, NonStrictContext& context) {}
void visit(AstExpr* expr, NonStrictContext& context)
{
auto pusher = pushStack(expr);
if (auto e = expr->as<AstExprGroup>())
return visit(e, context);
else if (auto e = expr->as<AstExprConstantNil>())
return visit(e, context);
else if (auto e = expr->as<AstExprConstantBool>())
return visit(e, context);
else if (auto e = expr->as<AstExprConstantNumber>())
return visit(e, context);
else if (auto e = expr->as<AstExprConstantString>())
return visit(e, context);
else if (auto e = expr->as<AstExprLocal>())
return visit(e, context);
else if (auto e = expr->as<AstExprGlobal>())
return visit(e, context);
else if (auto e = expr->as<AstExprVarargs>())
return visit(e, context);
else if (auto e = expr->as<AstExprCall>())
return visit(e, context);
else if (auto e = expr->as<AstExprIndexName>())
return visit(e, context);
else if (auto e = expr->as<AstExprIndexExpr>())
return visit(e, context);
else if (auto e = expr->as<AstExprFunction>())
return visit(e, context);
else if (auto e = expr->as<AstExprTable>())
return visit(e, context);
else if (auto e = expr->as<AstExprUnary>())
return visit(e, context);
else if (auto e = expr->as<AstExprBinary>())
return visit(e, context);
else if (auto e = expr->as<AstExprTypeAssertion>())
return visit(e, context);
else if (auto e = expr->as<AstExprIfElse>())
return visit(e, context);
else if (auto e = expr->as<AstExprInterpString>())
return visit(e, context);
else if (auto e = expr->as<AstExprError>())
return visit(e, context);
else
LUAU_ASSERT(!"NonStrictTypeChecker encountered an unknown expression type");
}
void visit(AstExprGroup* group, NonStrictContext& context) {}
void visit(AstExprConstantNil* expr, NonStrictContext& context) {}
void visit(AstExprConstantBool* expr, NonStrictContext& context) {}
void visit(AstExprConstantNumber* expr, NonStrictContext& context) {}
void visit(AstExprConstantString* expr, NonStrictContext& context) {}
void visit(AstExprLocal* local, NonStrictContext& context) {}
void visit(AstExprGlobal* global, NonStrictContext& context) {}
void visit(AstExprVarargs* global, NonStrictContext& context) {}
void visit(AstExprCall* call, NonStrictContext& context)
{
TypeId* originalCallTy = module->astOriginalCallTypes.find(call);
if (!originalCallTy)
return;
TypeId fnTy = *originalCallTy;
NonStrictContext fresh{};
if (auto fn = get<FunctionType>(follow(fnTy)))
{
if (fn->isCheckedFunction)
{
// We know fn is a checked function, which means it looks like:
// (S1, ... SN) -> T &
// (~S1, unknown^N-1) -> error &
// (unknown, ~S2, unknown^N-2) -> error
// ...
// ...
// (unknown^N-1, ~S_N) -> error
std::vector<TypeId> argTypes;
for (TypeId ty : fn->argTypes)
argTypes.push_back(ty);
// For a checked function, these gotta be the same size
LUAU_ASSERT(call->args.size == argTypes.size());
for (size_t i = 0; i < call->args.size; i++)
{
// For example, if the arg is "hi"
// The actual arg type is string
// The expected arg type is number
// The type of the argument in the overload is ~number
// We will compare arg and ~number
AstExpr* arg = call->args.data[i];
TypeId expectedArgType = argTypes[i];
NullableBreadcrumbId bc = dfg->getBreadcrumb(arg);
// TODO: Cache negations created here!!!
// See Jira Ticket: https://roblox.atlassian.net/browse/CLI-87539
if (bc)
{
TypeId runTimeErrorTy = arena.addType(NegationType{expectedArgType});
DefId def = bc->def;
fresh.context[def.get()] = runTimeErrorTy;
}
else
{
std::cout << "bad" << std::endl;
}
}
// Populate the context and now iterate through each of the arguments to the call to find out if we satisfy the types
for (size_t i = 0; i < call->args.size; i++)
{
AstExpr* arg = call->args.data[i];
// TODO: pipe in name of checked function to report Error
if (auto runTimeFailureType = willRunTimeError(arg, fresh))
reportError(CheckedFunctionCallError{argTypes[i], *runTimeFailureType, "", i}, arg->location);
}
}
}
}
void visit(AstExprIndexName* indexName, NonStrictContext& context) {}
void visit(AstExprIndexExpr* indexExpr, NonStrictContext& context) {}
void visit(AstExprFunction* exprFn, NonStrictContext& context)
{
auto pusher = pushStack(exprFn);
}
void visit(AstExprTable* table, NonStrictContext& context) {}
void visit(AstExprUnary* unary, NonStrictContext& context) {}
void visit(AstExprBinary* binary, NonStrictContext& context) {}
void visit(AstExprTypeAssertion* typeAssertion, NonStrictContext& context) {}
void visit(AstExprIfElse* ifElse, NonStrictContext& context) {}
void visit(AstExprInterpString* interpString, NonStrictContext& context) {}
void visit(AstExprError* error, NonStrictContext& context) {}
void reportError(TypeErrorData data, const Location& location)
{
module->errors.emplace_back(location, module->name, std::move(data));
// TODO: weave in logger here?
}
// If this fragment of the ast will run time error, return the type that causes this
std::optional<TypeId> willRunTimeError(AstExpr* fragment, const NonStrictContext& context)
{
if (NullableBreadcrumbId bc = dfg->getBreadcrumb(fragment))
{
std::optional<TypeId> contextTy = context.find(bc->def);
if (contextTy)
{
TypeId actualType = lookupType(fragment);
SubtypingResult r = subtyping.isSubtype(actualType, *contextTy);
if (r.normalizationTooComplex)
reportError(NormalizationTooComplex{}, fragment->location);
if (!r.isSubtype && !r.isErrorSuppressing)
reportError(TypeMismatch{actualType, *contextTy}, fragment->location);
if (r.isSubtype)
return {actualType};
}
}
return {};
}
}; };
void checkNonStrict(NotNull<BuiltinTypes> builtinTypes, Module* module) void checkNonStrict(NotNull<BuiltinTypes> builtinTypes, NotNull<InternalErrorReporter> ice, NotNull<UnifierSharedState> unifierState,
NotNull<const DataFlowGraph> dfg, NotNull<TypeCheckLimits> limits, const SourceModule& sourceModule, Module* module)
{ {
// TODO: unimplemented // TODO: unimplemented
NonStrictTypeChecker typeChecker{builtinTypes, ice, unifierState, dfg, limits, module};
typeChecker.visit(sourceModule.root);
unfreeze(module->interfaceTypes);
copyErrors(module->errors, module->interfaceTypes, builtinTypes);
freeze(module->interfaceTypes);
} }
} // namespace Luau } // namespace Luau

View file

@ -50,6 +50,8 @@ SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other)
// `|=` is intentional here, we want to preserve error related flags. // `|=` is intentional here, we want to preserve error related flags.
isErrorSuppressing |= other.isErrorSuppressing; isErrorSuppressing |= other.isErrorSuppressing;
normalizationTooComplex |= other.normalizationTooComplex; normalizationTooComplex |= other.normalizationTooComplex;
isCacheable &= other.isCacheable;
return *this; return *this;
} }
@ -58,6 +60,8 @@ SubtypingResult& SubtypingResult::orElse(const SubtypingResult& other)
isSubtype |= other.isSubtype; isSubtype |= other.isSubtype;
isErrorSuppressing |= other.isErrorSuppressing; isErrorSuppressing |= other.isErrorSuppressing;
normalizationTooComplex |= other.normalizationTooComplex; normalizationTooComplex |= other.normalizationTooComplex;
isCacheable &= other.isCacheable;
return *this; return *this;
} }
@ -86,14 +90,23 @@ SubtypingResult SubtypingResult::any(const std::vector<SubtypingResult>& results
return acc; return acc;
} }
Subtyping::Subtyping(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> typeArena, NotNull<Normalizer> normalizer,
NotNull<InternalErrorReporter> iceReporter, NotNull<Scope> scope)
: builtinTypes(builtinTypes)
, arena(typeArena)
, normalizer(normalizer)
, iceReporter(iceReporter)
, scope(scope)
{
}
SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy) SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy)
{ {
mappedGenerics.clear(); SubtypingEnvironment env;
mappedGenericPacks.clear();
SubtypingResult result = isCovariantWith(subTy, superTy); SubtypingResult result = isCovariantWith(env, subTy, superTy);
for (const auto& [subTy, bounds] : mappedGenerics) for (const auto& [subTy, bounds] : env.mappedGenerics)
{ {
const auto& lb = bounds.lowerBound; const auto& lb = bounds.lowerBound;
const auto& ub = bounds.upperBound; const auto& ub = bounds.upperBound;
@ -122,15 +135,42 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy)
result.isSubtype = false; result.isSubtype = false;
} }
result.andAlso(isCovariantWith(lowerBound, upperBound)); result.andAlso(isCovariantWith(env, lowerBound, upperBound));
} }
/* TODO: We presently don't store subtype test results in the persistent
* cache if the left-side type is a generic function.
*
* The implementation would be a bit tricky and we haven't seen any material
* impact on benchmarks.
*
* What we would want to do is to remember points within the type where
* mapped generics are introduced. When all the contingent generics are
* introduced at which we're doing the test, we can mark the result as
* cacheable.
*/
if (result.isCacheable)
resultCache[{subTy, superTy}] = result;
return result; return result;
} }
SubtypingResult Subtyping::isSubtype(TypePackId subTp, TypePackId superTp) SubtypingResult Subtyping::isSubtype(TypePackId subTp, TypePackId superTp)
{ {
return isCovariantWith(subTp, superTp); SubtypingEnvironment env;
return isCovariantWith(env, subTp, superTp);
}
SubtypingResult Subtyping::cache(SubtypingEnvironment& env, SubtypingResult result, TypeId subTy, TypeId superTy)
{
const std::pair<TypeId, TypeId> p{subTy, superTy};
if (result.isCacheable)
resultCache[p] = result;
else
env.ephemeralCache[p] = result;
return result;
} }
namespace namespace
@ -153,11 +193,19 @@ struct SeenSetPopper
}; };
} // namespace } // namespace
SubtypingResult Subtyping::isCovariantWith(TypeId subTy, TypeId superTy) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy)
{ {
subTy = follow(subTy); subTy = follow(subTy);
superTy = follow(superTy); superTy = follow(superTy);
SubtypingResult* cachedResult = resultCache.find({subTy, superTy});
if (cachedResult)
return *cachedResult;
cachedResult = env.ephemeralCache.find({subTy, superTy});
if (cachedResult)
return *cachedResult;
// TODO: Do we care about returning a proof that this is error-suppressing? // TODO: Do we care about returning a proof that this is error-suppressing?
// e.g. given `a | error <: a | error` where both operands are pointer equal, // e.g. given `a | error <: a | error` where both operands are pointer equal,
// then should it also carry the information that it's error-suppressing? // then should it also carry the information that it's error-suppressing?
@ -167,7 +215,29 @@ SubtypingResult Subtyping::isCovariantWith(TypeId subTy, TypeId superTy)
std::pair<TypeId, TypeId> typePair{subTy, superTy}; std::pair<TypeId, TypeId> typePair{subTy, superTy};
if (!seenTypes.insert(typePair).second) if (!seenTypes.insert(typePair).second)
return {true}; {
/* TODO: Caching results for recursive types is really tricky to think
* about.
*
* We'd like to cache at the outermost level where we encounter the
* recursive type, but we do not want to cache interior results that
* involve the cycle.
*
* Presently, we stop at cycles and assume that the subtype check will
* succeed because we'll eventually get there if it won't. However, if
* that cyclic type turns out not to have the asked-for subtyping
* relation, then all the intermediate cached results that were
* contingent on that assumption need to be evicted from the cache, or
* not entered into the cache, or something.
*
* For now, we do the conservative thing and refuse to cache anything
* that touches a cycle.
*/
SubtypingResult res;
res.isSubtype = true;
res.isCacheable = false;
return res;
}
SeenSetPopper ssp{&seenTypes, typePair}; SeenSetPopper ssp{&seenTypes, typePair};
@ -175,31 +245,31 @@ SubtypingResult Subtyping::isCovariantWith(TypeId subTy, TypeId superTy)
// tested as though it were its upper bounds. We do not yet support bounded // tested as though it were its upper bounds. We do not yet support bounded
// generics, so the upper bound is always unknown. // generics, so the upper bound is always unknown.
if (auto subGeneric = get<GenericType>(subTy); subGeneric && subsumes(subGeneric->scope, scope)) if (auto subGeneric = get<GenericType>(subTy); subGeneric && subsumes(subGeneric->scope, scope))
return isCovariantWith(builtinTypes->unknownType, superTy); return isCovariantWith(env, builtinTypes->unknownType, superTy);
if (auto superGeneric = get<GenericType>(superTy); superGeneric && subsumes(superGeneric->scope, scope)) if (auto superGeneric = get<GenericType>(superTy); superGeneric && subsumes(superGeneric->scope, scope))
return isCovariantWith(subTy, builtinTypes->unknownType); return isCovariantWith(env, subTy, builtinTypes->unknownType);
SubtypingResult result;
if (auto subUnion = get<UnionType>(subTy)) if (auto subUnion = get<UnionType>(subTy))
return isCovariantWith(subUnion, superTy); result = isCovariantWith(env, subUnion, superTy);
else if (auto superUnion = get<UnionType>(superTy)) else if (auto superUnion = get<UnionType>(superTy))
return isCovariantWith(subTy, superUnion); result = isCovariantWith(env, subTy, superUnion);
else if (auto superIntersection = get<IntersectionType>(superTy)) else if (auto superIntersection = get<IntersectionType>(superTy))
return isCovariantWith(subTy, superIntersection); result = isCovariantWith(env, subTy, superIntersection);
else if (auto subIntersection = get<IntersectionType>(subTy)) else if (auto subIntersection = get<IntersectionType>(subTy))
{ {
SubtypingResult result = isCovariantWith(subIntersection, superTy); result = isCovariantWith(env, subIntersection, superTy);
if (result.isSubtype || result.isErrorSuppressing || result.normalizationTooComplex) if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex)
return result; result = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy));
else
return isCovariantWith(normalizer->normalize(subTy), normalizer->normalize(superTy));
} }
else if (get<AnyType>(superTy)) else if (get<AnyType>(superTy))
return {true}; // This is always true. result = {true};
else if (get<AnyType>(subTy)) else if (get<AnyType>(subTy))
{ {
// any = unknown | error, so we rewrite this to match. // any = unknown | error, so we rewrite this to match.
// As per TAPL: A | B <: T iff A <: T && B <: T // As per TAPL: A | B <: T iff A <: T && B <: T
return isCovariantWith(builtinTypes->unknownType, superTy).andAlso(isCovariantWith(builtinTypes->errorType, superTy)); result = isCovariantWith(env, builtinTypes->unknownType, superTy).andAlso(isCovariantWith(env, builtinTypes->errorType, superTy));
} }
else if (get<UnknownType>(superTy)) else if (get<UnknownType>(superTy))
{ {
@ -208,57 +278,59 @@ SubtypingResult Subtyping::isCovariantWith(TypeId subTy, TypeId superTy)
LUAU_ASSERT(!get<IntersectionType>(subTy)); // TODO: replace with ice. LUAU_ASSERT(!get<IntersectionType>(subTy)); // TODO: replace with ice.
bool errorSuppressing = get<ErrorType>(subTy); bool errorSuppressing = get<ErrorType>(subTy);
return {!errorSuppressing, errorSuppressing}; result = {!errorSuppressing, errorSuppressing};
} }
else if (get<NeverType>(subTy)) else if (get<NeverType>(subTy))
return {true}; result = {true};
else if (get<ErrorType>(superTy)) else if (get<ErrorType>(superTy))
return {false, true}; result = {false, true};
else if (get<ErrorType>(subTy)) else if (get<ErrorType>(subTy))
return {false, true}; result = {false, true};
else if (auto p = get2<NegationType, NegationType>(subTy, superTy)) else if (auto p = get2<NegationType, NegationType>(subTy, superTy))
return isCovariantWith(p.first->ty, p.second->ty); result = isCovariantWith(env, p.first->ty, p.second->ty);
else if (auto subNegation = get<NegationType>(subTy)) else if (auto subNegation = get<NegationType>(subTy))
return isCovariantWith(subNegation, superTy); result = isCovariantWith(env, subNegation, superTy);
else if (auto superNegation = get<NegationType>(superTy)) else if (auto superNegation = get<NegationType>(superTy))
return isCovariantWith(subTy, superNegation); result = isCovariantWith(env, subTy, superNegation);
else if (auto subGeneric = get<GenericType>(subTy); subGeneric && variance == Variance::Covariant) else if (auto subGeneric = get<GenericType>(subTy); subGeneric && variance == Variance::Covariant)
{ {
bool ok = bindGeneric(subTy, superTy); bool ok = bindGeneric(env, subTy, superTy);
return {ok}; result.isSubtype = ok;
result.isCacheable = false;
} }
else if (auto superGeneric = get<GenericType>(superTy); superGeneric && variance == Variance::Contravariant) else if (auto superGeneric = get<GenericType>(superTy); superGeneric && variance == Variance::Contravariant)
{ {
bool ok = bindGeneric(subTy, superTy); bool ok = bindGeneric(env, subTy, superTy);
return {ok}; result.isSubtype = ok;
result.isCacheable = false;
} }
else if (auto p = get2<PrimitiveType, PrimitiveType>(subTy, superTy)) else if (auto p = get2<PrimitiveType, PrimitiveType>(subTy, superTy))
return isCovariantWith(p); result = isCovariantWith(env, p);
else if (auto p = get2<SingletonType, PrimitiveType>(subTy, superTy)) else if (auto p = get2<SingletonType, PrimitiveType>(subTy, superTy))
return isCovariantWith(p); result = isCovariantWith(env, p);
else if (auto p = get2<SingletonType, SingletonType>(subTy, superTy)) else if (auto p = get2<SingletonType, SingletonType>(subTy, superTy))
return isCovariantWith(p); result = isCovariantWith(env, p);
else if (auto p = get2<FunctionType, FunctionType>(subTy, superTy)) else if (auto p = get2<FunctionType, FunctionType>(subTy, superTy))
return isCovariantWith(p); result = isCovariantWith(env, p);
else if (auto p = get2<TableType, TableType>(subTy, superTy)) else if (auto p = get2<TableType, TableType>(subTy, superTy))
return isCovariantWith(p); result = isCovariantWith(env, p);
else if (auto p = get2<MetatableType, MetatableType>(subTy, superTy)) else if (auto p = get2<MetatableType, MetatableType>(subTy, superTy))
return isCovariantWith(p); result = isCovariantWith(env, p);
else if (auto p = get2<MetatableType, TableType>(subTy, superTy)) else if (auto p = get2<MetatableType, TableType>(subTy, superTy))
return isCovariantWith(p); result = isCovariantWith(env, p);
else if (auto p = get2<ClassType, ClassType>(subTy, superTy)) else if (auto p = get2<ClassType, ClassType>(subTy, superTy))
return isCovariantWith(p); result = isCovariantWith(env, p);
else if (auto p = get2<ClassType, TableType>(subTy, superTy)) else if (auto p = get2<ClassType, TableType>(subTy, superTy))
return isCovariantWith(p); result = isCovariantWith(env, p);
else if (auto p = get2<PrimitiveType, TableType>(subTy, superTy)) else if (auto p = get2<PrimitiveType, TableType>(subTy, superTy))
return isCovariantWith(p); result = isCovariantWith(env, p);
else if (auto p = get2<SingletonType, TableType>(subTy, superTy)) else if (auto p = get2<SingletonType, TableType>(subTy, superTy))
return isCovariantWith(p); result = isCovariantWith(env, p);
return {false}; return cache(env, result, subTy, superTy);
} }
SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp)
{ {
subTp = follow(subTp); subTp = follow(subTp);
superTp = follow(superTp); superTp = follow(superTp);
@ -278,7 +350,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
for (size_t i = 0; i < headSize; ++i) for (size_t i = 0; i < headSize; ++i)
{ {
results.push_back(isCovariantWith(subHead[i], superHead[i])); results.push_back(isCovariantWith(env, subHead[i], superHead[i]));
if (!results.back().isSubtype) if (!results.back().isSubtype)
return {false}; return {false};
} }
@ -292,7 +364,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
if (auto vt = get<VariadicTypePack>(*subTail)) if (auto vt = get<VariadicTypePack>(*subTail))
{ {
for (size_t i = headSize; i < superHead.size(); ++i) for (size_t i = headSize; i < superHead.size(); ++i)
results.push_back(isCovariantWith(vt->ty, superHead[i])); results.push_back(isCovariantWith(env, vt->ty, superHead[i]));
} }
else if (auto gt = get<GenericTypePack>(*subTail)) else if (auto gt = get<GenericTypePack>(*subTail))
{ {
@ -306,10 +378,10 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
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 = mappedGenericPacks.find(*subTail)) if (TypePackId* other = env.mappedGenericPacks.find(*subTail))
results.push_back(isCovariantWith(*other, superTailPack)); results.push_back(isCovariantWith(env, *other, superTailPack));
else else
mappedGenericPacks.try_insert(*subTail, superTailPack); env.mappedGenericPacks.try_insert(*subTail, superTailPack);
// FIXME? Not a fan of the early return here. It makes the // FIXME? Not a fan of the early return here. It makes the
// control flow harder to reason about. // control flow harder to reason about.
@ -337,7 +409,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
if (auto vt = get<VariadicTypePack>(*superTail)) if (auto vt = get<VariadicTypePack>(*superTail))
{ {
for (size_t i = headSize; i < subHead.size(); ++i) for (size_t i = headSize; i < subHead.size(); ++i)
results.push_back(isCovariantWith(subHead[i], vt->ty)); results.push_back(isCovariantWith(env, subHead[i], vt->ty));
} }
else if (auto gt = get<GenericTypePack>(*superTail)) else if (auto gt = get<GenericTypePack>(*superTail))
{ {
@ -351,10 +423,10 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
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 = mappedGenericPacks.find(*superTail)) if (TypePackId* other = env.mappedGenericPacks.find(*superTail))
results.push_back(isCovariantWith(*other, subTailPack)); results.push_back(isCovariantWith(env, *other, subTailPack));
else else
mappedGenericPacks.try_insert(*superTail, subTailPack); env.mappedGenericPacks.try_insert(*superTail, subTailPack);
// FIXME? Not a fan of the early return here. It makes the // FIXME? Not a fan of the early return here. It makes the
// control flow harder to reason about. // control flow harder to reason about.
@ -381,11 +453,11 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
{ {
if (auto p = get2<VariadicTypePack, VariadicTypePack>(*subTail, *superTail)) if (auto p = get2<VariadicTypePack, VariadicTypePack>(*subTail, *superTail))
{ {
results.push_back(isCovariantWith(p)); results.push_back(isCovariantWith(env, p));
} }
else if (auto p = get2<GenericTypePack, GenericTypePack>(*subTail, *superTail)) else if (auto p = get2<GenericTypePack, GenericTypePack>(*subTail, *superTail))
{ {
bool ok = bindGeneric(*subTail, *superTail); bool ok = bindGeneric(env, *subTail, *superTail);
results.push_back({ok}); results.push_back({ok});
} }
else if (get2<VariadicTypePack, GenericTypePack>(*subTail, *superTail)) else if (get2<VariadicTypePack, GenericTypePack>(*subTail, *superTail))
@ -393,7 +465,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
if (variance == Variance::Contravariant) if (variance == Variance::Contravariant)
{ {
// <A...>(A...) -> number <: (...number) -> number // <A...>(A...) -> number <: (...number) -> number
bool ok = bindGeneric(*subTail, *superTail); bool ok = bindGeneric(env, *subTail, *superTail);
results.push_back({ok}); results.push_back({ok});
} }
else else
@ -412,7 +484,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
else else
{ {
// <A...>() -> A... <: () -> ...number // <A...>() -> A... <: () -> ...number
bool ok = bindGeneric(*subTail, *superTail); bool ok = bindGeneric(env, *subTail, *superTail);
results.push_back({ok}); results.push_back({ok});
} }
} }
@ -428,7 +500,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
} }
else if (get<GenericTypePack>(*subTail)) else if (get<GenericTypePack>(*subTail))
{ {
bool ok = bindGeneric(*subTail, builtinTypes->emptyTypePack); bool ok = bindGeneric(env, *subTail, builtinTypes->emptyTypePack);
return {ok}; return {ok};
} }
else else
@ -452,7 +524,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
{ {
if (variance == Variance::Contravariant) if (variance == Variance::Contravariant)
{ {
bool ok = bindGeneric(builtinTypes->emptyTypePack, *superTail); bool ok = bindGeneric(env, builtinTypes->emptyTypePack, *superTail);
results.push_back({ok}); results.push_back({ok});
} }
else else
@ -466,33 +538,33 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
} }
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingResult Subtyping::isContravariantWith(SubTy&& subTy, SuperTy&& superTy) SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy)
{ {
return isCovariantWith(superTy, subTy); return isCovariantWith(env, superTy, subTy);
} }
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingResult Subtyping::isInvariantWith(SubTy&& subTy, SuperTy&& superTy) SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& superTy)
{ {
return isCovariantWith(subTy, superTy).andAlso(isContravariantWith(subTy, superTy)); return isCovariantWith(env, subTy, superTy).andAlso(isContravariantWith(env, subTy, superTy));
} }
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingResult Subtyping::isCovariantWith(const TryPair<const SubTy*, const SuperTy*>& pair) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TryPair<const SubTy*, const SuperTy*>& pair)
{ {
return isCovariantWith(pair.first, pair.second); return isCovariantWith(env, pair.first, pair.second);
} }
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingResult Subtyping::isContravariantWith(const TryPair<const SubTy*, const SuperTy*>& pair) SubtypingResult Subtyping::isContravariantWith(SubtypingEnvironment& env, const TryPair<const SubTy*, const SuperTy*>& pair)
{ {
return isCovariantWith(pair.second, pair.first); return isCovariantWith(env, pair.second, pair.first);
} }
template<typename SubTy, typename SuperTy> template<typename SubTy, typename SuperTy>
SubtypingResult Subtyping::isInvariantWith(const TryPair<const SubTy*, const SuperTy*>& pair) SubtypingResult Subtyping::isInvariantWith(SubtypingEnvironment& env, const TryPair<const SubTy*, const SuperTy*>& pair)
{ {
return isCovariantWith(pair).andAlso(isContravariantWith(pair)); return isCovariantWith(env, pair).andAlso(isContravariantWith(pair));
} }
/* /*
@ -526,43 +598,43 @@ SubtypingResult Subtyping::isInvariantWith(const TryPair<const SubTy*, const Sup
* other just asks for boolean ~ 'b. We can dispatch this and only commit * other just asks for boolean ~ 'b. We can dispatch this and only commit
* boolean ~ 'b. This constraint does not teach us anything about 'a. * boolean ~ 'b. This constraint does not teach us anything about 'a.
*/ */
SubtypingResult Subtyping::isCovariantWith(TypeId subTy, const UnionType* superUnion) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const UnionType* superUnion)
{ {
// As per TAPL: T <: A | B iff T <: A || T <: B // As per TAPL: T <: A | B iff T <: A || T <: B
std::vector<SubtypingResult> subtypings; std::vector<SubtypingResult> subtypings;
for (TypeId ty : superUnion) for (TypeId ty : superUnion)
subtypings.push_back(isCovariantWith(subTy, ty)); subtypings.push_back(isCovariantWith(env, subTy, ty));
return SubtypingResult::any(subtypings); return SubtypingResult::any(subtypings);
} }
SubtypingResult Subtyping::isCovariantWith(const UnionType* subUnion, TypeId superTy) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const UnionType* subUnion, TypeId superTy)
{ {
// As per TAPL: A | B <: T iff A <: T && B <: T // As per TAPL: A | B <: T iff A <: T && B <: T
std::vector<SubtypingResult> subtypings; std::vector<SubtypingResult> subtypings;
for (TypeId ty : subUnion) for (TypeId ty : subUnion)
subtypings.push_back(isCovariantWith(ty, superTy)); subtypings.push_back(isCovariantWith(env, ty, superTy));
return SubtypingResult::all(subtypings); return SubtypingResult::all(subtypings);
} }
SubtypingResult Subtyping::isCovariantWith(TypeId subTy, const IntersectionType* superIntersection) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const IntersectionType* superIntersection)
{ {
// As per TAPL: T <: A & B iff T <: A && T <: B // As per TAPL: T <: A & B iff T <: A && T <: B
std::vector<SubtypingResult> subtypings; std::vector<SubtypingResult> subtypings;
for (TypeId ty : superIntersection) for (TypeId ty : superIntersection)
subtypings.push_back(isCovariantWith(subTy, ty)); subtypings.push_back(isCovariantWith(env, subTy, ty));
return SubtypingResult::all(subtypings); return SubtypingResult::all(subtypings);
} }
SubtypingResult Subtyping::isCovariantWith(const IntersectionType* subIntersection, TypeId superTy) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const IntersectionType* subIntersection, TypeId superTy)
{ {
// As per TAPL: A & B <: T iff A <: T || B <: T // As per TAPL: A & B <: T iff A <: T || B <: T
std::vector<SubtypingResult> subtypings; std::vector<SubtypingResult> subtypings;
for (TypeId ty : subIntersection) for (TypeId ty : subIntersection)
subtypings.push_back(isCovariantWith(ty, superTy)); subtypings.push_back(isCovariantWith(env, ty, superTy));
return SubtypingResult::any(subtypings); return SubtypingResult::any(subtypings);
} }
SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, TypeId superTy) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NegationType* subNegation, TypeId superTy)
{ {
TypeId negatedTy = follow(subNegation->ty); TypeId negatedTy = follow(subNegation->ty);
@ -572,17 +644,17 @@ SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, Type
if (is<NeverType>(negatedTy)) if (is<NeverType>(negatedTy))
{ {
// ¬never ~ unknown // ¬never ~ unknown
return isCovariantWith(builtinTypes->unknownType, superTy); return isCovariantWith(env, builtinTypes->unknownType, superTy);
} }
else if (is<UnknownType>(negatedTy)) else if (is<UnknownType>(negatedTy))
{ {
// ¬unknown ~ never // ¬unknown ~ never
return isCovariantWith(builtinTypes->neverType, superTy); return isCovariantWith(env, builtinTypes->neverType, superTy);
} }
else if (is<AnyType>(negatedTy)) else if (is<AnyType>(negatedTy))
{ {
// ¬any ~ any // ¬any ~ any
return isCovariantWith(negatedTy, superTy); return isCovariantWith(env, negatedTy, superTy);
} }
else if (auto u = get<UnionType>(negatedTy)) else if (auto u = get<UnionType>(negatedTy))
{ {
@ -593,7 +665,7 @@ SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, Type
for (TypeId ty : u) for (TypeId ty : u)
{ {
NegationType negatedTmp{ty}; NegationType negatedTmp{ty};
subtypings.push_back(isCovariantWith(&negatedTmp, superTy)); subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy));
} }
return SubtypingResult::all(subtypings); return SubtypingResult::all(subtypings);
@ -607,11 +679,11 @@ SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, Type
for (TypeId ty : i) for (TypeId ty : i)
{ {
if (auto negatedPart = get<NegationType>(follow(ty))) if (auto negatedPart = get<NegationType>(follow(ty)))
subtypings.push_back(isCovariantWith(negatedPart->ty, superTy)); subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy));
else else
{ {
NegationType negatedTmp{ty}; NegationType negatedTmp{ty};
subtypings.push_back(isCovariantWith(&negatedTmp, superTy)); subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy));
} }
} }
@ -629,19 +701,19 @@ SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, Type
} }
} }
SubtypingResult Subtyping::isCovariantWith(const TypeId subTy, const NegationType* superNegation) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TypeId subTy, const NegationType* superNegation)
{ {
TypeId negatedTy = follow(superNegation->ty); TypeId negatedTy = follow(superNegation->ty);
if (is<NeverType>(negatedTy)) if (is<NeverType>(negatedTy))
{ {
// ¬never ~ unknown // ¬never ~ unknown
return isCovariantWith(subTy, builtinTypes->unknownType); return isCovariantWith(env, subTy, builtinTypes->unknownType);
} }
else if (is<UnknownType>(negatedTy)) else if (is<UnknownType>(negatedTy))
{ {
// ¬unknown ~ never // ¬unknown ~ never
return isCovariantWith(subTy, builtinTypes->neverType); return isCovariantWith(env, subTy, builtinTypes->neverType);
} }
else if (is<AnyType>(negatedTy)) else if (is<AnyType>(negatedTy))
{ {
@ -657,11 +729,11 @@ SubtypingResult Subtyping::isCovariantWith(const TypeId subTy, const NegationTyp
for (TypeId ty : u) for (TypeId ty : u)
{ {
if (auto negatedPart = get<NegationType>(follow(ty))) if (auto negatedPart = get<NegationType>(follow(ty)))
subtypings.push_back(isCovariantWith(subTy, negatedPart->ty)); subtypings.push_back(isCovariantWith(env, subTy, negatedPart->ty));
else else
{ {
NegationType negatedTmp{ty}; NegationType negatedTmp{ty};
subtypings.push_back(isCovariantWith(subTy, &negatedTmp)); subtypings.push_back(isCovariantWith(env, subTy, &negatedTmp));
} }
} }
@ -676,11 +748,11 @@ SubtypingResult Subtyping::isCovariantWith(const TypeId subTy, const NegationTyp
for (TypeId ty : i) for (TypeId ty : i)
{ {
if (auto negatedPart = get<NegationType>(follow(ty))) if (auto negatedPart = get<NegationType>(follow(ty)))
subtypings.push_back(isCovariantWith(subTy, negatedPart->ty)); subtypings.push_back(isCovariantWith(env, subTy, negatedPart->ty));
else else
{ {
NegationType negatedTmp{ty}; NegationType negatedTmp{ty};
subtypings.push_back(isCovariantWith(subTy, &negatedTmp)); subtypings.push_back(isCovariantWith(env, subTy, &negatedTmp));
} }
} }
@ -724,7 +796,7 @@ SubtypingResult Subtyping::isCovariantWith(const TypeId subTy, const NegationTyp
else if (auto p = get2<SingletonType, SingletonType>(subTy, negatedTy)) else if (auto p = get2<SingletonType, SingletonType>(subTy, negatedTy))
return {*p.first != *p.second}; return {*p.first != *p.second};
else if (auto p = get2<ClassType, ClassType>(subTy, negatedTy)) else if (auto p = get2<ClassType, ClassType>(subTy, negatedTy))
return SubtypingResult::negate(isCovariantWith(p.first, p.second)); return SubtypingResult::negate(isCovariantWith(env, p.first, p.second));
else if (get2<FunctionType, ClassType>(subTy, negatedTy)) else if (get2<FunctionType, ClassType>(subTy, negatedTy))
return {true}; return {true};
else if (is<ErrorType, FunctionType, TableType, MetatableType>(negatedTy)) else if (is<ErrorType, FunctionType, TableType, MetatableType>(negatedTy))
@ -733,12 +805,12 @@ SubtypingResult Subtyping::isCovariantWith(const TypeId subTy, const NegationTyp
return {false}; return {false};
} }
SubtypingResult Subtyping::isCovariantWith(const PrimitiveType* subPrim, const PrimitiveType* superPrim) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const PrimitiveType* superPrim)
{ {
return {subPrim->type == superPrim->type}; return {subPrim->type == superPrim->type};
} }
SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, const PrimitiveType* superPrim) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const PrimitiveType* superPrim)
{ {
if (get<StringSingleton>(subSingleton) && superPrim->type == PrimitiveType::String) if (get<StringSingleton>(subSingleton) && superPrim->type == PrimitiveType::String)
return {true}; return {true};
@ -748,12 +820,12 @@ SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, co
return {false}; return {false};
} }
SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, const SingletonType* superSingleton) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const SingletonType* superSingleton)
{ {
return {*subSingleton == *superSingleton}; return {*subSingleton == *superSingleton};
} }
SubtypingResult Subtyping::isCovariantWith(const TableType* subTable, const TableType* superTable) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TableType* subTable, const TableType* superTable)
{ {
SubtypingResult result{true}; SubtypingResult result{true};
@ -764,12 +836,12 @@ SubtypingResult Subtyping::isCovariantWith(const TableType* subTable, const Tabl
{ {
std::vector<SubtypingResult> results; std::vector<SubtypingResult> results;
if (auto it = subTable->props.find(name); it != subTable->props.end()) if (auto it = subTable->props.find(name); it != subTable->props.end())
results.push_back(isInvariantWith(it->second.type(), prop.type())); results.push_back(isInvariantWith(env, it->second.type(), prop.type()));
if (subTable->indexer) if (subTable->indexer)
{ {
if (isInvariantWith(subTable->indexer->indexType, builtinTypes->stringType).isSubtype) if (isInvariantWith(env, subTable->indexer->indexType, builtinTypes->stringType).isSubtype)
results.push_back(isInvariantWith(subTable->indexer->indexResultType, prop.type())); results.push_back(isInvariantWith(env, subTable->indexer->indexResultType, prop.type()));
} }
if (results.empty()) if (results.empty())
@ -781,7 +853,7 @@ SubtypingResult Subtyping::isCovariantWith(const TableType* subTable, const Tabl
if (superTable->indexer) if (superTable->indexer)
{ {
if (subTable->indexer) if (subTable->indexer)
result.andAlso(isInvariantWith(*subTable->indexer, *superTable->indexer)); result.andAlso(isInvariantWith(env, *subTable->indexer, *superTable->indexer));
else else
return {false}; return {false};
} }
@ -789,12 +861,12 @@ SubtypingResult Subtyping::isCovariantWith(const TableType* subTable, const Tabl
return result; return result;
} }
SubtypingResult Subtyping::isCovariantWith(const MetatableType* subMt, const MetatableType* superMt) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const MetatableType* superMt)
{ {
return isCovariantWith(subMt->table, superMt->table).andAlso(isCovariantWith(subMt->metatable, superMt->metatable)); return isCovariantWith(env, subMt->table, superMt->table).andAlso(isCovariantWith(env, subMt->metatable, superMt->metatable));
} }
SubtypingResult Subtyping::isCovariantWith(const MetatableType* subMt, const TableType* superTable) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable)
{ {
if (auto subTable = get<TableType>(subMt->table)) if (auto subTable = get<TableType>(subMt->table))
{ {
@ -807,7 +879,7 @@ SubtypingResult Subtyping::isCovariantWith(const MetatableType* subMt, const Tab
// that the metatable isn't a subtype of the table, even though they have // that the metatable isn't a subtype of the table, even though they have
// compatible properties/shapes. We'll revisit this later when we have a // compatible properties/shapes. We'll revisit this later when we have a
// better understanding of how important this is. // better understanding of how important this is.
return isCovariantWith(subTable, superTable); return isCovariantWith(env, subTable, superTable);
} }
else else
{ {
@ -816,19 +888,19 @@ SubtypingResult Subtyping::isCovariantWith(const MetatableType* subMt, const Tab
} }
} }
SubtypingResult Subtyping::isCovariantWith(const ClassType* subClass, const ClassType* superClass) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const ClassType* superClass)
{ {
return {isSubclass(subClass, superClass)}; return {isSubclass(subClass, superClass)};
} }
SubtypingResult Subtyping::isCovariantWith(const ClassType* subClass, const TableType* superTable) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const TableType* superTable)
{ {
SubtypingResult result{true}; SubtypingResult result{true};
for (const auto& [name, prop] : superTable->props) for (const auto& [name, prop] : superTable->props)
{ {
if (auto classProp = lookupClassProp(subClass, name)) if (auto classProp = lookupClassProp(subClass, name))
result.andAlso(isInvariantWith(prop.type(), classProp->type())); result.andAlso(isInvariantWith(env, prop.type(), classProp->type()));
else else
return SubtypingResult{false}; return SubtypingResult{false};
} }
@ -836,20 +908,20 @@ SubtypingResult Subtyping::isCovariantWith(const ClassType* subClass, const Tabl
return result; return result;
} }
SubtypingResult Subtyping::isCovariantWith(const FunctionType* subFunction, const FunctionType* superFunction) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const FunctionType* subFunction, const FunctionType* superFunction)
{ {
SubtypingResult result; SubtypingResult result;
{ {
VarianceFlipper vf{&variance}; VarianceFlipper vf{&variance};
result.orElse(isContravariantWith(subFunction->argTypes, superFunction->argTypes)); result.orElse(isContravariantWith(env, subFunction->argTypes, superFunction->argTypes));
} }
result.andAlso(isCovariantWith(subFunction->retTypes, superFunction->retTypes)); result.andAlso(isCovariantWith(env, subFunction->retTypes, superFunction->retTypes));
return result; return result;
} }
SubtypingResult Subtyping::isCovariantWith(const PrimitiveType* subPrim, const TableType* superTable) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const TableType* superTable)
{ {
SubtypingResult result{false}; SubtypingResult result{false};
if (subPrim->type == PrimitiveType::String) if (subPrim->type == PrimitiveType::String)
@ -861,7 +933,7 @@ SubtypingResult Subtyping::isCovariantWith(const PrimitiveType* subPrim, const T
if (auto it = mttv->props.find("__index"); it != mttv->props.end()) if (auto it = mttv->props.find("__index"); it != mttv->props.end())
{ {
if (auto stringTable = get<TableType>(it->second.type())) if (auto stringTable = get<TableType>(it->second.type()))
result.orElse(isCovariantWith(stringTable, superTable)); result.orElse(isCovariantWith(env, stringTable, superTable));
} }
} }
} }
@ -870,7 +942,7 @@ SubtypingResult Subtyping::isCovariantWith(const PrimitiveType* subPrim, const T
return result; return result;
} }
SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, const TableType* superTable) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const TableType* superTable)
{ {
SubtypingResult result{false}; SubtypingResult result{false};
if (auto stringleton = get<StringSingleton>(subSingleton)) if (auto stringleton = get<StringSingleton>(subSingleton))
@ -882,7 +954,7 @@ SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, co
if (auto it = mttv->props.find("__index"); it != mttv->props.end()) if (auto it = mttv->props.find("__index"); it != mttv->props.end())
{ {
if (auto stringTable = get<TableType>(it->second.type())) if (auto stringTable = get<TableType>(it->second.type()))
result.orElse(isCovariantWith(stringTable, superTable)); result.orElse(isCovariantWith(env, stringTable, superTable));
} }
} }
} }
@ -890,32 +962,33 @@ SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, co
return result; return result;
} }
SubtypingResult Subtyping::isCovariantWith(const TableIndexer& subIndexer, const TableIndexer& superIndexer) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TableIndexer& subIndexer, const TableIndexer& superIndexer)
{ {
return isInvariantWith(subIndexer.indexType, superIndexer.indexType).andAlso(isInvariantWith(superIndexer.indexResultType, subIndexer.indexResultType)); return isInvariantWith(env, subIndexer.indexType, superIndexer.indexType)
.andAlso(isInvariantWith(env, superIndexer.indexResultType, subIndexer.indexResultType));
} }
SubtypingResult Subtyping::isCovariantWith(const NormalizedType* subNorm, const NormalizedType* superNorm) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm)
{ {
if (!subNorm || !superNorm) if (!subNorm || !superNorm)
return {false, true, true}; return {false, true, true};
SubtypingResult result = isCovariantWith(subNorm->tops, superNorm->tops); SubtypingResult result = isCovariantWith(env, subNorm->tops, superNorm->tops);
result.andAlso(isCovariantWith(subNorm->booleans, superNorm->booleans)); result.andAlso(isCovariantWith(env, subNorm->booleans, superNorm->booleans));
result.andAlso(isCovariantWith(subNorm->classes, superNorm->classes).orElse(isCovariantWith(subNorm->classes, superNorm->tables))); result.andAlso(isCovariantWith(env, subNorm->classes, superNorm->classes).orElse(isCovariantWith(env, subNorm->classes, superNorm->tables)));
result.andAlso(isCovariantWith(subNorm->errors, superNorm->errors)); result.andAlso(isCovariantWith(env, subNorm->errors, superNorm->errors));
result.andAlso(isCovariantWith(subNorm->nils, superNorm->nils)); result.andAlso(isCovariantWith(env, subNorm->nils, superNorm->nils));
result.andAlso(isCovariantWith(subNorm->numbers, superNorm->numbers)); result.andAlso(isCovariantWith(env, subNorm->numbers, superNorm->numbers));
result.andAlso(isCovariantWith(subNorm->strings, superNorm->strings)); result.andAlso(isCovariantWith(env, subNorm->strings, superNorm->strings));
result.andAlso(isCovariantWith(subNorm->strings, superNorm->tables)); result.andAlso(isCovariantWith(env, subNorm->strings, superNorm->tables));
result.andAlso(isCovariantWith(subNorm->threads, superNorm->threads)); result.andAlso(isCovariantWith(env, subNorm->threads, superNorm->threads));
result.andAlso(isCovariantWith(subNorm->tables, superNorm->tables)); result.andAlso(isCovariantWith(env, subNorm->tables, superNorm->tables));
result.andAlso(isCovariantWith(subNorm->functions, superNorm->functions)); result.andAlso(isCovariantWith(env, subNorm->functions, superNorm->functions));
// isCovariantWith(subNorm->tyvars, superNorm->tyvars); // isCovariantWith(subNorm->tyvars, superNorm->tyvars);
return result; return result;
} }
SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass, const NormalizedClassType& superClass) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const NormalizedClassType& superClass)
{ {
for (const auto& [subClassTy, _] : subClass.classes) for (const auto& [subClassTy, _] : subClass.classes)
{ {
@ -923,13 +996,13 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass,
for (const auto& [superClassTy, superNegations] : superClass.classes) for (const auto& [superClassTy, superNegations] : superClass.classes)
{ {
result.orElse(isCovariantWith(subClassTy, superClassTy)); result.orElse(isCovariantWith(env, subClassTy, superClassTy));
if (!result.isSubtype) if (!result.isSubtype)
continue; continue;
for (TypeId negation : superNegations) for (TypeId negation : superNegations)
{ {
result.andAlso(SubtypingResult::negate(isCovariantWith(subClassTy, negation))); result.andAlso(SubtypingResult::negate(isCovariantWith(env, subClassTy, negation)));
if (result.isSubtype) if (result.isSubtype)
break; break;
} }
@ -942,14 +1015,14 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass,
return {true}; return {true};
} }
SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass, const TypeIds& superTables) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const TypeIds& superTables)
{ {
for (const auto& [subClassTy, _] : subClass.classes) for (const auto& [subClassTy, _] : subClass.classes)
{ {
SubtypingResult result; SubtypingResult result;
for (TypeId superTableTy : superTables) for (TypeId superTableTy : superTables)
result.orElse(isCovariantWith(subClassTy, superTableTy)); result.orElse(isCovariantWith(env, subClassTy, superTableTy));
if (!result.isSubtype) if (!result.isSubtype)
return result; return result;
@ -958,13 +1031,13 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass,
return {true}; return {true};
} }
SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString, const NormalizedStringType& superString) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedStringType& subString, const NormalizedStringType& superString)
{ {
bool isSubtype = Luau::isSubtype(subString, superString); bool isSubtype = Luau::isSubtype(subString, superString);
return {isSubtype}; return {isSubtype};
} }
SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString, const TypeIds& superTables) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const NormalizedStringType& subString, const TypeIds& superTables)
{ {
if (subString.isNever()) if (subString.isNever())
return {true}; return {true};
@ -974,7 +1047,7 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString
SubtypingResult result; SubtypingResult result;
for (const auto& superTable : superTables) for (const auto& superTable : superTables)
{ {
result.orElse(isCovariantWith(builtinTypes->stringType, superTable)); result.orElse(isCovariantWith(env, builtinTypes->stringType, superTable));
if (result.isSubtype) if (result.isSubtype)
return result; return result;
} }
@ -990,7 +1063,7 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString
SubtypingResult result{true}; SubtypingResult result{true};
for (const auto& [_, subString] : subString.singletons) for (const auto& [_, subString] : subString.singletons)
{ {
result.andAlso(isCovariantWith(subString, superTable)); result.andAlso(isCovariantWith(env, subString, superTable));
if (!result.isSubtype) if (!result.isSubtype)
break; break;
} }
@ -1004,17 +1077,18 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString
return {false}; return {false};
} }
SubtypingResult Subtyping::isCovariantWith(const NormalizedFunctionType& subFunction, const NormalizedFunctionType& superFunction) SubtypingResult Subtyping::isCovariantWith(
SubtypingEnvironment& env, const NormalizedFunctionType& subFunction, const NormalizedFunctionType& superFunction)
{ {
if (subFunction.isNever()) if (subFunction.isNever())
return {true}; return {true};
else if (superFunction.isTop) else if (superFunction.isTop)
return {true}; return {true};
else else
return isCovariantWith(subFunction.parts, superFunction.parts); return isCovariantWith(env, subFunction.parts, superFunction.parts);
} }
SubtypingResult Subtyping::isCovariantWith(const TypeIds& subTypes, const TypeIds& superTypes) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TypeIds& subTypes, const TypeIds& superTypes)
{ {
std::vector<SubtypingResult> results; std::vector<SubtypingResult> results;
@ -1022,32 +1096,32 @@ SubtypingResult Subtyping::isCovariantWith(const TypeIds& subTypes, const TypeId
{ {
results.emplace_back(); results.emplace_back();
for (TypeId superTy : superTypes) for (TypeId superTy : superTypes)
results.back().orElse(isCovariantWith(subTy, superTy)); results.back().orElse(isCovariantWith(env, subTy, superTy));
} }
return SubtypingResult::all(results); return SubtypingResult::all(results);
} }
SubtypingResult Subtyping::isCovariantWith(const VariadicTypePack* subVariadic, const VariadicTypePack* superVariadic) SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const VariadicTypePack* subVariadic, const VariadicTypePack* superVariadic)
{ {
return isCovariantWith(subVariadic->ty, superVariadic->ty); return isCovariantWith(env, subVariadic->ty, superVariadic->ty);
} }
bool Subtyping::bindGeneric(TypeId subTy, TypeId superTy) bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypeId subTy, TypeId superTy)
{ {
if (variance == Variance::Covariant) if (variance == Variance::Covariant)
{ {
if (!get<GenericType>(subTy)) if (!get<GenericType>(subTy))
return false; return false;
mappedGenerics[subTy].upperBound.insert(superTy); env.mappedGenerics[subTy].upperBound.insert(superTy);
} }
else else
{ {
if (!get<GenericType>(superTy)) if (!get<GenericType>(superTy))
return false; return false;
mappedGenerics[superTy].lowerBound.insert(subTy); env.mappedGenerics[superTy].lowerBound.insert(subTy);
} }
return true; return true;
@ -1058,7 +1132,7 @@ bool Subtyping::bindGeneric(TypeId subTy, TypeId superTy)
* side, it is permissible to tentatively bind that generic to the right side * side, it is permissible to tentatively bind that generic to the right side
* type. * type.
*/ */
bool Subtyping::bindGeneric(TypePackId subTp, TypePackId superTp) bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp)
{ {
if (variance == Variance::Contravariant) if (variance == Variance::Contravariant)
std::swap(superTp, subTp); std::swap(superTp, subTp);
@ -1066,10 +1140,10 @@ bool Subtyping::bindGeneric(TypePackId subTp, TypePackId superTp)
if (!get<GenericTypePack>(subTp)) if (!get<GenericTypePack>(subTp))
return false; return false;
if (TypePackId* m = mappedGenericPacks.find(subTp)) if (TypePackId* m = env.mappedGenericPacks.find(subTp))
return *m == superTp; return *m == superTp;
mappedGenericPacks[subTp] = superTp; env.mappedGenericPacks[subTp] = superTp;
return true; return true;
} }

View file

@ -8,6 +8,7 @@
#include "Luau/Normalize.h" #include "Luau/Normalize.h"
#include "Luau/Simplify.h" #include "Luau/Simplify.h"
#include "Luau/Substitution.h" #include "Luau/Substitution.h"
#include "Luau/Subtyping.h"
#include "Luau/ToString.h" #include "Luau/ToString.h"
#include "Luau/TxnLog.h" #include "Luau/TxnLog.h"
#include "Luau/TypeCheckLimits.h" #include "Luau/TypeCheckLimits.h"
@ -320,8 +321,25 @@ bool isPending(TypeId ty, ConstraintSolver* solver)
return is<BlockedType>(ty) || is<PendingExpansionType>(ty) || is<TypeFamilyInstanceType>(ty) || (solver && solver->hasUnresolvedConstraints(ty)); return is<BlockedType>(ty) || is<PendingExpansionType>(ty) || is<TypeFamilyInstanceType>(ty) || (solver && solver->hasUnresolvedConstraints(ty));
} }
TypeFamilyReductionResult<TypeId> notFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 1 || !packParams.empty())
{
ctx->ice->ice("not type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
TypeId ty = follow(typeParams.at(0));
if (isPending(ty, ctx->solver))
return {std::nullopt, false, {ty}, {}};
// `not` operates on anything and returns a `boolean` always.
return {ctx->builtins->booleanType, false, {}, {}};
}
TypeFamilyReductionResult<TypeId> numericBinopFamilyFn( TypeFamilyReductionResult<TypeId> numericBinopFamilyFn(
std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx, const std::string metamethod) const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx, const std::string metamethod)
{ {
if (typeParams.size() != 2 || !packParams.empty()) if (typeParams.size() != 2 || !packParams.empty())
{ {
@ -333,34 +351,28 @@ TypeFamilyReductionResult<TypeId> numericBinopFamilyFn(
TypeId rhsTy = follow(typeParams.at(1)); TypeId rhsTy = follow(typeParams.at(1));
const NormalizedType* normLhsTy = ctx->normalizer->normalize(lhsTy); const NormalizedType* normLhsTy = ctx->normalizer->normalize(lhsTy);
const NormalizedType* normRhsTy = ctx->normalizer->normalize(rhsTy); const NormalizedType* normRhsTy = ctx->normalizer->normalize(rhsTy);
// if either failed to normalize, we can't reduce, but know nothing about inhabitance.
if (!normLhsTy || !normRhsTy) if (!normLhsTy || !normRhsTy)
{
return {std::nullopt, false, {}, {}}; return {std::nullopt, false, {}, {}};
}
else if (is<AnyType>(normLhsTy->tops) || is<AnyType>(normRhsTy->tops)) // if one of the types is error suppressing, we can reduce to `any` since we should suppress errors in the result of the usage.
{ if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors())
return {ctx->builtins->anyType, false, {}, {}}; return {ctx->builtins->anyType, false, {}, {}};
}
else if ((normLhsTy->hasNumbers() || normLhsTy->hasTops()) && (normRhsTy->hasNumbers() || normRhsTy->hasTops())) // if we have a `never`, we can never observe that the numeric operator didn't work.
{ if (is<NeverType>(lhsTy) || is<NeverType>(rhsTy))
return {ctx->builtins->numberType, false, {}, {}};
}
else if (is<ErrorType>(lhsTy) || is<ErrorType>(rhsTy))
{
return {ctx->builtins->errorRecoveryType(), false, {}, {}};
}
else if (is<NeverType>(lhsTy) || is<NeverType>(rhsTy))
{
return {ctx->builtins->neverType, false, {}, {}}; return {ctx->builtins->neverType, false, {}, {}};
}
else if (isPending(lhsTy, ctx->solver)) // if we're adding two `number` types, the result is `number`.
{ if (normLhsTy->isExactlyNumber() && normRhsTy->isExactlyNumber())
return {ctx->builtins->numberType, false, {}, {}};
// otherwise, check if we need to wait on either type to be further resolved
if (isPending(lhsTy, ctx->solver))
return {std::nullopt, false, {lhsTy}, {}}; return {std::nullopt, false, {lhsTy}, {}};
}
else if (isPending(rhsTy, ctx->solver)) else if (isPending(rhsTy, ctx->solver))
{
return {std::nullopt, false, {rhsTy}, {}}; return {std::nullopt, false, {rhsTy}, {}};
}
// 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.
@ -385,39 +397,32 @@ TypeFamilyReductionResult<TypeId> numericBinopFamilyFn(
if (!mmFtv) if (!mmFtv)
return {std::nullopt, true, {}, {}}; return {std::nullopt, true, {}, {}};
if (std::optional<TypeId> instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType)) std::optional<TypeId> instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType);
{ if (!instantiatedMmType)
if (const FunctionType* instantiatedMmFtv = get<FunctionType>(*instantiatedMmType)) return {std::nullopt, true, {}, {}};
{
std::vector<TypeId> inferredArgs; const FunctionType* instantiatedMmFtv = get<FunctionType>(*instantiatedMmType);
if (!reversed) if (!instantiatedMmFtv)
inferredArgs = {lhsTy, rhsTy}; return {ctx->builtins->errorRecoveryType(), false, {}, {}};
else
inferredArgs = {rhsTy, lhsTy}; std::vector<TypeId> inferredArgs;
if (!reversed)
TypePackId inferredArgPack = ctx->arena->addTypePack(std::move(inferredArgs)); inferredArgs = {lhsTy, rhsTy};
Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice}; else
if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes)) inferredArgs = {rhsTy, lhsTy};
return {std::nullopt, true, {}, {}}; // occurs check failed
TypePackId inferredArgPack = ctx->arena->addTypePack(std::move(inferredArgs));
if (std::optional<TypeId> ret = first(instantiatedMmFtv->retTypes)) Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice};
return {*ret, false, {}, {}}; if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes))
else return {std::nullopt, true, {}, {}}; // occurs check failed
return {std::nullopt, true, {}, {}};
} if (std::optional<TypeId> ret = first(instantiatedMmFtv->retTypes))
else return {*ret, false, {}, {}};
{ else
return {ctx->builtins->errorRecoveryType(), false, {}, {}};
}
}
else
{
// TODO: Not the nicest logic here.
return {std::nullopt, true, {}, {}}; return {std::nullopt, true, {}, {}};
}
} }
TypeFamilyReductionResult<TypeId> addFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx) TypeFamilyReductionResult<TypeId> addFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{ {
if (typeParams.size() != 2 || !packParams.empty()) if (typeParams.size() != 2 || !packParams.empty())
{ {
@ -428,7 +433,7 @@ TypeFamilyReductionResult<TypeId> addFamilyFn(std::vector<TypeId> typeParams, st
return numericBinopFamilyFn(typeParams, packParams, ctx, "__add"); return numericBinopFamilyFn(typeParams, packParams, ctx, "__add");
} }
TypeFamilyReductionResult<TypeId> subFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx) TypeFamilyReductionResult<TypeId> subFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{ {
if (typeParams.size() != 2 || !packParams.empty()) if (typeParams.size() != 2 || !packParams.empty())
{ {
@ -439,7 +444,7 @@ TypeFamilyReductionResult<TypeId> subFamilyFn(std::vector<TypeId> typeParams, st
return numericBinopFamilyFn(typeParams, packParams, ctx, "__sub"); return numericBinopFamilyFn(typeParams, packParams, ctx, "__sub");
} }
TypeFamilyReductionResult<TypeId> mulFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx) TypeFamilyReductionResult<TypeId> mulFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{ {
if (typeParams.size() != 2 || !packParams.empty()) if (typeParams.size() != 2 || !packParams.empty())
{ {
@ -450,7 +455,7 @@ TypeFamilyReductionResult<TypeId> mulFamilyFn(std::vector<TypeId> typeParams, st
return numericBinopFamilyFn(typeParams, packParams, ctx, "__mul"); return numericBinopFamilyFn(typeParams, packParams, ctx, "__mul");
} }
TypeFamilyReductionResult<TypeId> divFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx) TypeFamilyReductionResult<TypeId> divFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{ {
if (typeParams.size() != 2 || !packParams.empty()) if (typeParams.size() != 2 || !packParams.empty())
{ {
@ -461,7 +466,7 @@ TypeFamilyReductionResult<TypeId> divFamilyFn(std::vector<TypeId> typeParams, st
return numericBinopFamilyFn(typeParams, packParams, ctx, "__div"); return numericBinopFamilyFn(typeParams, packParams, ctx, "__div");
} }
TypeFamilyReductionResult<TypeId> idivFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx) TypeFamilyReductionResult<TypeId> idivFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{ {
if (typeParams.size() != 2 || !packParams.empty()) if (typeParams.size() != 2 || !packParams.empty())
{ {
@ -472,7 +477,7 @@ TypeFamilyReductionResult<TypeId> idivFamilyFn(std::vector<TypeId> typeParams, s
return numericBinopFamilyFn(typeParams, packParams, ctx, "__idiv"); return numericBinopFamilyFn(typeParams, packParams, ctx, "__idiv");
} }
TypeFamilyReductionResult<TypeId> powFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx) TypeFamilyReductionResult<TypeId> powFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{ {
if (typeParams.size() != 2 || !packParams.empty()) if (typeParams.size() != 2 || !packParams.empty())
{ {
@ -483,7 +488,7 @@ TypeFamilyReductionResult<TypeId> powFamilyFn(std::vector<TypeId> typeParams, st
return numericBinopFamilyFn(typeParams, packParams, ctx, "__pow"); return numericBinopFamilyFn(typeParams, packParams, ctx, "__pow");
} }
TypeFamilyReductionResult<TypeId> modFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx) TypeFamilyReductionResult<TypeId> modFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{ {
if (typeParams.size() != 2 || !packParams.empty()) if (typeParams.size() != 2 || !packParams.empty())
{ {
@ -494,7 +499,91 @@ TypeFamilyReductionResult<TypeId> modFamilyFn(std::vector<TypeId> typeParams, st
return numericBinopFamilyFn(typeParams, packParams, ctx, "__mod"); return numericBinopFamilyFn(typeParams, packParams, ctx, "__mod");
} }
TypeFamilyReductionResult<TypeId> andFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx) TypeFamilyReductionResult<TypeId> concatFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("concat type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
TypeId lhsTy = follow(typeParams.at(0));
TypeId rhsTy = follow(typeParams.at(1));
const NormalizedType* normLhsTy = ctx->normalizer->normalize(lhsTy);
const NormalizedType* normRhsTy = ctx->normalizer->normalize(rhsTy);
// if either failed to normalize, we can't reduce, but know nothing about inhabitance.
if (!normLhsTy || !normRhsTy)
return {std::nullopt, false, {}, {}};
// if one of the types is error suppressing, we can reduce to `any` since we should suppress errors in the result of the usage.
if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors())
return {ctx->builtins->anyType, false, {}, {}};
// if we have a `never`, we can never observe that the numeric operator didn't work.
if (is<NeverType>(lhsTy) || is<NeverType>(rhsTy))
return {ctx->builtins->neverType, false, {}, {}};
// if we're concatenating two elements that are either strings or numbers, the result is `string`.
if ((normLhsTy->isSubtypeOfString() || normLhsTy->isExactlyNumber()) && (normRhsTy->isSubtypeOfString() || normRhsTy->isExactlyNumber()))
return {ctx->builtins->stringType, false, {}, {}};
// otherwise, check if we need to wait on either type to be further resolved
if (isPending(lhsTy, ctx->solver))
return {std::nullopt, false, {lhsTy}, {}};
else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, false, {rhsTy}, {}};
// 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.
ErrorVec dummy;
std::optional<TypeId> mmType = findMetatableEntry(ctx->builtins, dummy, lhsTy, "__concat", Location{});
bool reversed = false;
if (!mmType)
{
mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, "__concat", Location{});
reversed = true;
}
if (!mmType)
return {std::nullopt, true, {}, {}};
mmType = follow(*mmType);
if (isPending(*mmType, ctx->solver))
return {std::nullopt, false, {*mmType}, {}};
const FunctionType* mmFtv = get<FunctionType>(*mmType);
if (!mmFtv)
return {std::nullopt, true, {}, {}};
std::optional<TypeId> instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType);
if (!instantiatedMmType)
return {std::nullopt, true, {}, {}};
const FunctionType* instantiatedMmFtv = get<FunctionType>(*instantiatedMmType);
if (!instantiatedMmFtv)
return {ctx->builtins->errorRecoveryType(), false, {}, {}};
std::vector<TypeId> inferredArgs;
if (!reversed)
inferredArgs = {lhsTy, rhsTy};
else
inferredArgs = {rhsTy, lhsTy};
TypePackId inferredArgPack = ctx->arena->addTypePack(std::move(inferredArgs));
Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice};
if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes))
return {std::nullopt, true, {}, {}}; // occurs check failed
Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->ice, ctx->scope};
if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes).isSubtype) // TODO: is this the right variance?
return {std::nullopt, true, {}, {}};
return {ctx->builtins->stringType, false, {}, {}};
}
TypeFamilyReductionResult<TypeId> andFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{ {
if (typeParams.size() != 2 || !packParams.empty()) if (typeParams.size() != 2 || !packParams.empty())
{ {
@ -522,7 +611,7 @@ TypeFamilyReductionResult<TypeId> andFamilyFn(std::vector<TypeId> typeParams, st
return {overallResult.result, false, std::move(blockedTypes), {}}; return {overallResult.result, false, std::move(blockedTypes), {}};
} }
TypeFamilyReductionResult<TypeId> orFamilyFn(std::vector<TypeId> typeParams, std::vector<TypePackId> packParams, NotNull<TypeFamilyContext> ctx) TypeFamilyReductionResult<TypeId> orFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{ {
if (typeParams.size() != 2 || !packParams.empty()) if (typeParams.size() != 2 || !packParams.empty())
{ {
@ -550,16 +639,196 @@ TypeFamilyReductionResult<TypeId> orFamilyFn(std::vector<TypeId> typeParams, std
return {overallResult.result, false, std::move(blockedTypes), {}}; return {overallResult.result, false, std::move(blockedTypes), {}};
} }
static TypeFamilyReductionResult<TypeId> comparisonFamilyFn(
const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx, const std::string metamethod)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
TypeId lhsTy = follow(typeParams.at(0));
TypeId rhsTy = follow(typeParams.at(1));
const NormalizedType* normLhsTy = ctx->normalizer->normalize(lhsTy);
const NormalizedType* normRhsTy = ctx->normalizer->normalize(rhsTy);
// if either failed to normalize, we can't reduce, but know nothing about inhabitance.
if (!normLhsTy || !normRhsTy)
return {std::nullopt, false, {}, {}};
// if one of the types is error suppressing, we can just go ahead and reduce.
if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors())
return {ctx->builtins->booleanType, false, {}, {}};
// if we have a `never`, we can never observe that the comparison didn't work.
if (is<NeverType>(lhsTy) || is<NeverType>(rhsTy))
return {ctx->builtins->booleanType, false, {}, {}};
// If both types are some strict subset of `string`, we can reduce now.
if (normLhsTy->isSubtypeOfString() && normRhsTy->isSubtypeOfString())
return {ctx->builtins->booleanType, false, {}, {}};
// If both types are exactly `number`, we can reduce now.
if (normLhsTy->isExactlyNumber() && normRhsTy->isExactlyNumber())
return {ctx->builtins->booleanType, false, {}, {}};
// otherwise, check if we need to wait on either type to be further resolved
if (isPending(lhsTy, ctx->solver))
return {std::nullopt, false, {lhsTy}, {}};
else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, false, {rhsTy}, {}};
// 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.
ErrorVec dummy;
std::optional<TypeId> mmType = findMetatableEntry(ctx->builtins, dummy, lhsTy, metamethod, Location{});
if (!mmType)
mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, metamethod, Location{});
if (!mmType)
return {std::nullopt, true, {}, {}};
mmType = follow(*mmType);
if (isPending(*mmType, ctx->solver))
return {std::nullopt, false, {*mmType}, {}};
const FunctionType* mmFtv = get<FunctionType>(*mmType);
if (!mmFtv)
return {std::nullopt, true, {}, {}};
std::optional<TypeId> instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType);
if (!instantiatedMmType)
return {std::nullopt, true, {}, {}};
const FunctionType* instantiatedMmFtv = get<FunctionType>(*instantiatedMmType);
if (!instantiatedMmFtv)
return {ctx->builtins->errorRecoveryType(), false, {}, {}};
TypePackId inferredArgPack = ctx->arena->addTypePack({lhsTy, rhsTy});
Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice};
if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes))
return {std::nullopt, true, {}, {}}; // occurs check failed
Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->ice, ctx->scope};
if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes).isSubtype) // TODO: is this the right variance?
return {std::nullopt, true, {}, {}};
return {ctx->builtins->booleanType, false, {}, {}};
}
TypeFamilyReductionResult<TypeId> ltFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("lt type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
return comparisonFamilyFn(typeParams, packParams, ctx, "__lt");
}
TypeFamilyReductionResult<TypeId> leFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("le type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
return comparisonFamilyFn(typeParams, packParams, ctx, "__le");
}
TypeFamilyReductionResult<TypeId> eqFamilyFn(const std::vector<TypeId>& typeParams, const std::vector<TypePackId>& packParams, NotNull<TypeFamilyContext> ctx)
{
if (typeParams.size() != 2 || !packParams.empty())
{
ctx->ice->ice("eq type family: encountered a type family instance without the required argument structure");
LUAU_ASSERT(false);
}
TypeId lhsTy = follow(typeParams.at(0));
TypeId rhsTy = follow(typeParams.at(1));
const NormalizedType* normLhsTy = ctx->normalizer->normalize(lhsTy);
const NormalizedType* normRhsTy = ctx->normalizer->normalize(rhsTy);
// if either failed to normalize, we can't reduce, but know nothing about inhabitance.
if (!normLhsTy || !normRhsTy)
return {std::nullopt, false, {}, {}};
// if one of the types is error suppressing, we can just go ahead and reduce.
if (normLhsTy->shouldSuppressErrors() || normRhsTy->shouldSuppressErrors())
return {ctx->builtins->booleanType, false, {}, {}};
// if we have a `never`, we can never observe that the comparison didn't work.
if (is<NeverType>(lhsTy) || is<NeverType>(rhsTy))
return {ctx->builtins->booleanType, false, {}, {}};
// otherwise, check if we need to wait on either type to be further resolved
if (isPending(lhsTy, ctx->solver))
return {std::nullopt, false, {lhsTy}, {}};
else if (isPending(rhsTy, ctx->solver))
return {std::nullopt, false, {rhsTy}, {}};
// 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.
ErrorVec dummy;
std::optional<TypeId> mmType = findMetatableEntry(ctx->builtins, dummy, lhsTy, "__eq", Location{});
if (!mmType)
mmType = findMetatableEntry(ctx->builtins, dummy, rhsTy, "__eq", Location{});
// if neither type has a metatable entry for `__eq`, then we'll check for inhabitance of the intersection!
if (!mmType && ctx->normalizer->isIntersectionInhabited(lhsTy, rhsTy))
return {ctx->builtins->booleanType, false, {}, {}}; // if it's inhabited, everything is okay!
else if (!mmType)
return {std::nullopt, true, {}, {}}; // if it's not, then this family is irreducible!
mmType = follow(*mmType);
if (isPending(*mmType, ctx->solver))
return {std::nullopt, false, {*mmType}, {}};
const FunctionType* mmFtv = get<FunctionType>(*mmType);
if (!mmFtv)
return {std::nullopt, true, {}, {}};
std::optional<TypeId> instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType);
if (!instantiatedMmType)
return {std::nullopt, true, {}, {}};
const FunctionType* instantiatedMmFtv = get<FunctionType>(*instantiatedMmType);
if (!instantiatedMmFtv)
return {ctx->builtins->errorRecoveryType(), false, {}, {}};
TypePackId inferredArgPack = ctx->arena->addTypePack({lhsTy, rhsTy});
Unifier2 u2{ctx->arena, ctx->builtins, ctx->scope, ctx->ice};
if (!u2.unify(inferredArgPack, instantiatedMmFtv->argTypes))
return {std::nullopt, true, {}, {}}; // occurs check failed
Subtyping subtyping{ctx->builtins, ctx->arena, ctx->normalizer, ctx->ice, ctx->scope};
if (!subtyping.isSubtype(inferredArgPack, instantiatedMmFtv->argTypes).isSubtype) // TODO: is this the right variance?
return {std::nullopt, true, {}, {}};
return {ctx->builtins->booleanType, false, {}, {}};
}
BuiltinTypeFamilies::BuiltinTypeFamilies() BuiltinTypeFamilies::BuiltinTypeFamilies()
: addFamily{"Add", addFamilyFn} : notFamily{"not", notFamilyFn}
, subFamily{"Sub", subFamilyFn} , addFamily{"add", addFamilyFn}
, mulFamily{"Mul", mulFamilyFn} , subFamily{"sub", subFamilyFn}
, divFamily{"Div", divFamilyFn} , mulFamily{"mul", mulFamilyFn}
, idivFamily{"FloorDiv", idivFamilyFn} , divFamily{"div", divFamilyFn}
, powFamily{"Exp", powFamilyFn} , idivFamily{"idiv", idivFamilyFn}
, modFamily{"Mod", modFamilyFn} , powFamily{"pow", powFamilyFn}
, andFamily{"And", andFamilyFn} , modFamily{"mod", modFamilyFn}
, orFamily{"Or", orFamilyFn} , concatFamily{"concat", concatFamilyFn}
, andFamily{"and", andFamilyFn}
, orFamily{"or", orFamilyFn}
, ltFamily{"lt", ltFamilyFn}
, leFamily{"le", leFamilyFn}
, eqFamily{"eq", eqFamilyFn}
{ {
} }
@ -582,6 +851,11 @@ void BuiltinTypeFamilies::addToScope(NotNull<TypeArena> arena, NotNull<Scope> sc
scope->exportedTypeBindings[idivFamily.name] = mkBinaryTypeFamily(&idivFamily); scope->exportedTypeBindings[idivFamily.name] = mkBinaryTypeFamily(&idivFamily);
scope->exportedTypeBindings[powFamily.name] = mkBinaryTypeFamily(&powFamily); scope->exportedTypeBindings[powFamily.name] = mkBinaryTypeFamily(&powFamily);
scope->exportedTypeBindings[modFamily.name] = mkBinaryTypeFamily(&modFamily); scope->exportedTypeBindings[modFamily.name] = mkBinaryTypeFamily(&modFamily);
scope->exportedTypeBindings[concatFamily.name] = mkBinaryTypeFamily(&concatFamily);
scope->exportedTypeBindings[ltFamily.name] = mkBinaryTypeFamily(&ltFamily);
scope->exportedTypeBindings[leFamily.name] = mkBinaryTypeFamily(&leFamily);
scope->exportedTypeBindings[eqFamily.name] = mkBinaryTypeFamily(&eqFamily);
} }
} // namespace Luau } // namespace Luau

View file

@ -12,7 +12,6 @@
#include "Luau/TypeUtils.h" #include "Luau/TypeUtils.h"
#include "Luau/Type.h" #include "Luau/Type.h"
#include "Luau/VisitType.h" #include "Luau/VisitType.h"
#include "Luau/TypeFamily.h"
#include <algorithm> #include <algorithm>
@ -453,32 +452,10 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
blockedTypes.push_back(superTy); blockedTypes.push_back(superTy);
if (log.get<TypeFamilyInstanceType>(superTy)) if (log.get<TypeFamilyInstanceType>(superTy))
{ ice("Unexpected TypeFamilyInstanceType superTy");
// FIXME: we should be be ICEing here because the old unifier is legacy and should not interact with type families at all.
// Unfortunately, there are, at the time of writing, still uses of the old unifier under local type inference.
TypeCheckLimits limits;
reduceFamilies(
superTy, location, TypeFamilyContext{NotNull(types), builtinTypes, scope, normalizer, NotNull{sharedState.iceHandler}, NotNull{&limits}});
superTy = log.follow(superTy);
}
if (log.get<TypeFamilyInstanceType>(subTy)) if (log.get<TypeFamilyInstanceType>(subTy))
{ ice("Unexpected TypeFamilyInstanceType subTy");
// FIXME: we should be be ICEing here because the old unifier is legacy and should not interact with type families at all.
// Unfortunately, there are, at the time of writing, still uses of the old unifier under local type inference.
TypeCheckLimits limits;
reduceFamilies(
subTy, location, TypeFamilyContext{NotNull(types), builtinTypes, scope, normalizer, NotNull{sharedState.iceHandler}, NotNull{&limits}});
subTy = log.follow(subTy);
}
// If we can't reduce the families down and we still have type family types
// here, we are stuck. Nothing meaningful can be done here. We don't wish to
// report an error, either.
if (log.get<TypeFamilyInstanceType>(superTy) || log.get<TypeFamilyInstanceType>(subTy))
{
return;
}
auto superFree = log.getMutable<FreeType>(superTy); auto superFree = log.getMutable<FreeType>(superTy);
auto subFree = log.getMutable<FreeType>(subTy); auto subFree = log.getMutable<FreeType>(subTy);

View file

@ -374,7 +374,7 @@ public:
AstExprFunction(const Location& location, const AstArray<AstGenericType>& generics, const AstArray<AstGenericTypePack>& genericPacks, AstExprFunction(const Location& location, const AstArray<AstGenericType>& generics, const AstArray<AstGenericTypePack>& genericPacks,
AstLocal* self, const AstArray<AstLocal*>& args, bool vararg, const Location& varargLocation, AstStatBlock* body, size_t functionDepth, AstLocal* self, const AstArray<AstLocal*>& args, bool vararg, const Location& varargLocation, AstStatBlock* body, size_t functionDepth,
const AstName& debugname, const std::optional<AstTypeList>& returnAnnotation = {}, AstTypePack* varargAnnotation = nullptr, const AstName& debugname, const std::optional<AstTypeList>& returnAnnotation = {}, AstTypePack* varargAnnotation = nullptr,
bool hasEnd = false, const std::optional<Location>& argLocation = std::nullopt); bool DEPRECATED_hasEnd = false, const std::optional<Location>& argLocation = std::nullopt);
void visit(AstVisitor* visitor) override; void visit(AstVisitor* visitor) override;
@ -393,7 +393,8 @@ public:
AstName debugname; AstName debugname;
bool hasEnd = false; // TODO clip with FFlag::LuauClipExtraHasEndProps
bool DEPRECATED_hasEnd = false;
std::optional<Location> argLocation; std::optional<Location> argLocation;
}; };
@ -539,6 +540,17 @@ public:
void visit(AstVisitor* visitor) override; void visit(AstVisitor* visitor) override;
AstArray<AstStat*> body; AstArray<AstStat*> body;
/* Indicates whether or not this block has been terminated in a
* syntactically valid way.
*
* This is usually but not always done with the 'end' keyword. AstStatIf
* and AstStatRepeat are the two main exceptions to this.
*
* The 'then' clause of an if statement can properly be closed by the
* keywords 'else' or 'elseif'. A 'repeat' loop's body is closed with the
* 'until' keyword.
*/
bool hasEnd = false; bool hasEnd = false;
}; };
@ -548,7 +560,7 @@ public:
LUAU_RTTI(AstStatIf) LUAU_RTTI(AstStatIf)
AstStatIf(const Location& location, AstExpr* condition, AstStatBlock* thenbody, AstStat* elsebody, const std::optional<Location>& thenLocation, AstStatIf(const Location& location, AstExpr* condition, AstStatBlock* thenbody, AstStat* elsebody, const std::optional<Location>& thenLocation,
const std::optional<Location>& elseLocation, bool hasEnd); const std::optional<Location>& elseLocation, bool DEPRECATED_hasEnd);
void visit(AstVisitor* visitor) override; void visit(AstVisitor* visitor) override;
@ -561,7 +573,8 @@ public:
// Active for 'elseif' as well // Active for 'elseif' as well
std::optional<Location> elseLocation; std::optional<Location> elseLocation;
bool hasEnd = false; // TODO clip with FFlag::LuauClipExtraHasEndProps
bool DEPRECATED_hasEnd = false;
}; };
class AstStatWhile : public AstStat class AstStatWhile : public AstStat
@ -569,7 +582,7 @@ class AstStatWhile : public AstStat
public: public:
LUAU_RTTI(AstStatWhile) LUAU_RTTI(AstStatWhile)
AstStatWhile(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasDo, const Location& doLocation, bool hasEnd); AstStatWhile(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasDo, const Location& doLocation, bool DEPRECATED_hasEnd);
void visit(AstVisitor* visitor) override; void visit(AstVisitor* visitor) override;
@ -579,7 +592,8 @@ public:
bool hasDo = false; bool hasDo = false;
Location doLocation; Location doLocation;
bool hasEnd = false; // TODO clip with FFlag::LuauClipExtraHasEndProps
bool DEPRECATED_hasEnd = false;
}; };
class AstStatRepeat : public AstStat class AstStatRepeat : public AstStat
@ -587,14 +601,14 @@ class AstStatRepeat : public AstStat
public: public:
LUAU_RTTI(AstStatRepeat) LUAU_RTTI(AstStatRepeat)
AstStatRepeat(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasUntil); AstStatRepeat(const Location& location, AstExpr* condition, AstStatBlock* body, bool DEPRECATED_hasUntil);
void visit(AstVisitor* visitor) override; void visit(AstVisitor* visitor) override;
AstExpr* condition; AstExpr* condition;
AstStatBlock* body; AstStatBlock* body;
bool hasUntil = false; bool DEPRECATED_hasUntil = false;
}; };
class AstStatBreak : public AstStat class AstStatBreak : public AstStat
@ -663,7 +677,7 @@ public:
LUAU_RTTI(AstStatFor) LUAU_RTTI(AstStatFor)
AstStatFor(const Location& location, AstLocal* var, AstExpr* from, AstExpr* to, AstExpr* step, AstStatBlock* body, bool hasDo, AstStatFor(const Location& location, AstLocal* var, AstExpr* from, AstExpr* to, AstExpr* step, AstStatBlock* body, bool hasDo,
const Location& doLocation, bool hasEnd); const Location& doLocation, bool DEPRECATED_hasEnd);
void visit(AstVisitor* visitor) override; void visit(AstVisitor* visitor) override;
@ -676,7 +690,8 @@ public:
bool hasDo = false; bool hasDo = false;
Location doLocation; Location doLocation;
bool hasEnd = false; // TODO clip with FFlag::LuauClipExtraHasEndProps
bool DEPRECATED_hasEnd = false;
}; };
class AstStatForIn : public AstStat class AstStatForIn : public AstStat
@ -685,7 +700,7 @@ public:
LUAU_RTTI(AstStatForIn) LUAU_RTTI(AstStatForIn)
AstStatForIn(const Location& location, const AstArray<AstLocal*>& vars, const AstArray<AstExpr*>& values, AstStatBlock* body, bool hasIn, AstStatForIn(const Location& location, const AstArray<AstLocal*>& vars, const AstArray<AstExpr*>& values, AstStatBlock* body, bool hasIn,
const Location& inLocation, bool hasDo, const Location& doLocation, bool hasEnd); const Location& inLocation, bool hasDo, const Location& doLocation, bool DEPRECATED_hasEnd);
void visit(AstVisitor* visitor) override; void visit(AstVisitor* visitor) override;
@ -699,7 +714,8 @@ public:
bool hasDo = false; bool hasDo = false;
Location doLocation; Location doLocation;
bool hasEnd = false; // TODO clip with FFlag::LuauClipExtraHasEndProps
bool DEPRECATED_hasEnd = false;
}; };
class AstStatAssign : public AstStat class AstStatAssign : public AstStat

View file

@ -3,7 +3,7 @@
#include "Luau/Common.h" #include "Luau/Common.h"
LUAU_FASTFLAG(LuauFloorDivision) LUAU_FASTFLAG(LuauFloorDivision);
namespace Luau namespace Luau
{ {
@ -164,7 +164,7 @@ void AstExprIndexExpr::visit(AstVisitor* visitor)
AstExprFunction::AstExprFunction(const Location& location, const AstArray<AstGenericType>& generics, const AstArray<AstGenericTypePack>& genericPacks, AstExprFunction::AstExprFunction(const Location& location, const AstArray<AstGenericType>& generics, const AstArray<AstGenericTypePack>& genericPacks,
AstLocal* self, const AstArray<AstLocal*>& args, bool vararg, const Location& varargLocation, AstStatBlock* body, size_t functionDepth, AstLocal* self, const AstArray<AstLocal*>& args, bool vararg, const Location& varargLocation, AstStatBlock* body, size_t functionDepth,
const AstName& debugname, const std::optional<AstTypeList>& returnAnnotation, AstTypePack* varargAnnotation, bool hasEnd, const AstName& debugname, const std::optional<AstTypeList>& returnAnnotation, AstTypePack* varargAnnotation, bool DEPRECATED_hasEnd,
const std::optional<Location>& argLocation) const std::optional<Location>& argLocation)
: AstExpr(ClassIndex(), location) : AstExpr(ClassIndex(), location)
, generics(generics) , generics(generics)
@ -178,7 +178,7 @@ AstExprFunction::AstExprFunction(const Location& location, const AstArray<AstGen
, body(body) , body(body)
, functionDepth(functionDepth) , functionDepth(functionDepth)
, debugname(debugname) , debugname(debugname)
, hasEnd(hasEnd) , DEPRECATED_hasEnd(DEPRECATED_hasEnd)
, argLocation(argLocation) , argLocation(argLocation)
{ {
} }
@ -397,14 +397,14 @@ void AstStatBlock::visit(AstVisitor* visitor)
} }
AstStatIf::AstStatIf(const Location& location, AstExpr* condition, AstStatBlock* thenbody, AstStat* elsebody, AstStatIf::AstStatIf(const Location& location, AstExpr* condition, AstStatBlock* thenbody, AstStat* elsebody,
const std::optional<Location>& thenLocation, const std::optional<Location>& elseLocation, bool hasEnd) const std::optional<Location>& thenLocation, const std::optional<Location>& elseLocation, bool DEPRECATED_hasEnd)
: AstStat(ClassIndex(), location) : AstStat(ClassIndex(), location)
, condition(condition) , condition(condition)
, thenbody(thenbody) , thenbody(thenbody)
, elsebody(elsebody) , elsebody(elsebody)
, thenLocation(thenLocation) , thenLocation(thenLocation)
, elseLocation(elseLocation) , elseLocation(elseLocation)
, hasEnd(hasEnd) , DEPRECATED_hasEnd(DEPRECATED_hasEnd)
{ {
} }
@ -420,13 +420,13 @@ void AstStatIf::visit(AstVisitor* visitor)
} }
} }
AstStatWhile::AstStatWhile(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasDo, const Location& doLocation, bool hasEnd) AstStatWhile::AstStatWhile(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasDo, const Location& doLocation, bool DEPRECATED_hasEnd)
: AstStat(ClassIndex(), location) : AstStat(ClassIndex(), location)
, condition(condition) , condition(condition)
, body(body) , body(body)
, hasDo(hasDo) , hasDo(hasDo)
, doLocation(doLocation) , doLocation(doLocation)
, hasEnd(hasEnd) , DEPRECATED_hasEnd(DEPRECATED_hasEnd)
{ {
} }
@ -439,11 +439,11 @@ void AstStatWhile::visit(AstVisitor* visitor)
} }
} }
AstStatRepeat::AstStatRepeat(const Location& location, AstExpr* condition, AstStatBlock* body, bool hasUntil) AstStatRepeat::AstStatRepeat(const Location& location, AstExpr* condition, AstStatBlock* body, bool DEPRECATED_hasUntil)
: AstStat(ClassIndex(), location) : AstStat(ClassIndex(), location)
, condition(condition) , condition(condition)
, body(body) , body(body)
, hasUntil(hasUntil) , DEPRECATED_hasUntil(DEPRECATED_hasUntil)
{ {
} }
@ -528,7 +528,7 @@ void AstStatLocal::visit(AstVisitor* visitor)
} }
AstStatFor::AstStatFor(const Location& location, AstLocal* var, AstExpr* from, AstExpr* to, AstExpr* step, AstStatBlock* body, bool hasDo, AstStatFor::AstStatFor(const Location& location, AstLocal* var, AstExpr* from, AstExpr* to, AstExpr* step, AstStatBlock* body, bool hasDo,
const Location& doLocation, bool hasEnd) const Location& doLocation, bool DEPRECATED_hasEnd)
: AstStat(ClassIndex(), location) : AstStat(ClassIndex(), location)
, var(var) , var(var)
, from(from) , from(from)
@ -537,7 +537,7 @@ AstStatFor::AstStatFor(const Location& location, AstLocal* var, AstExpr* from, A
, body(body) , body(body)
, hasDo(hasDo) , hasDo(hasDo)
, doLocation(doLocation) , doLocation(doLocation)
, hasEnd(hasEnd) , DEPRECATED_hasEnd(DEPRECATED_hasEnd)
{ {
} }
@ -559,7 +559,7 @@ void AstStatFor::visit(AstVisitor* visitor)
} }
AstStatForIn::AstStatForIn(const Location& location, const AstArray<AstLocal*>& vars, const AstArray<AstExpr*>& values, AstStatBlock* body, AstStatForIn::AstStatForIn(const Location& location, const AstArray<AstLocal*>& vars, const AstArray<AstExpr*>& values, AstStatBlock* body,
bool hasIn, const Location& inLocation, bool hasDo, const Location& doLocation, bool hasEnd) bool hasIn, const Location& inLocation, bool hasDo, const Location& doLocation, bool DEPRECATED_hasEnd)
: AstStat(ClassIndex(), location) : AstStat(ClassIndex(), location)
, vars(vars) , vars(vars)
, values(values) , values(values)
@ -568,7 +568,7 @@ AstStatForIn::AstStatForIn(const Location& location, const AstArray<AstLocal*>&
, inLocation(inLocation) , inLocation(inLocation)
, hasDo(hasDo) , hasDo(hasDo)
, doLocation(doLocation) , doLocation(doLocation)
, hasEnd(hasEnd) , DEPRECATED_hasEnd(DEPRECATED_hasEnd)
{ {
} }

View file

@ -15,6 +15,7 @@
LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000) LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000)
LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100) LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
LUAU_FASTFLAGVARIABLE(LuauParseDeclareClassIndexer, false) LUAU_FASTFLAGVARIABLE(LuauParseDeclareClassIndexer, false)
LUAU_FASTFLAGVARIABLE(LuauClipExtraHasEndProps, false)
LUAU_FASTFLAG(LuauFloorDivision) LUAU_FASTFLAG(LuauFloorDivision)
LUAU_FASTFLAG(LuauCheckedFunctionSyntax) LUAU_FASTFLAG(LuauCheckedFunctionSyntax)
@ -371,16 +372,18 @@ AstStat* Parser::parseIf()
AstStat* elsebody = nullptr; AstStat* elsebody = nullptr;
Location end = start; Location end = start;
std::optional<Location> elseLocation; std::optional<Location> elseLocation;
bool hasEnd = false; bool DEPRECATED_hasEnd = false;
if (lexer.current().type == Lexeme::ReservedElseif) if (lexer.current().type == Lexeme::ReservedElseif)
{ {
if (FFlag::LuauClipExtraHasEndProps)
thenbody->hasEnd = true;
unsigned int recursionCounterOld = recursionCounter; unsigned int recursionCounterOld = recursionCounter;
incrementRecursionCounter("elseif"); incrementRecursionCounter("elseif");
elseLocation = lexer.current().location; elseLocation = lexer.current().location;
elsebody = parseIf(); elsebody = parseIf();
end = elsebody->location; end = elsebody->location;
hasEnd = elsebody->as<AstStatIf>()->hasEnd; DEPRECATED_hasEnd = elsebody->as<AstStatIf>()->DEPRECATED_hasEnd;
recursionCounter = recursionCounterOld; recursionCounter = recursionCounterOld;
} }
else else
@ -389,6 +392,8 @@ AstStat* Parser::parseIf()
if (lexer.current().type == Lexeme::ReservedElse) if (lexer.current().type == Lexeme::ReservedElse)
{ {
if (FFlag::LuauClipExtraHasEndProps)
thenbody->hasEnd = true;
elseLocation = lexer.current().location; elseLocation = lexer.current().location;
matchThenElse = lexer.current(); matchThenElse = lexer.current();
nextLexeme(); nextLexeme();
@ -399,10 +404,22 @@ AstStat* Parser::parseIf()
end = lexer.current().location; end = lexer.current().location;
hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchThenElse); bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchThenElse);
DEPRECATED_hasEnd = hasEnd;
if (FFlag::LuauClipExtraHasEndProps)
{
if (elsebody)
{
if (AstStatBlock* elseBlock = elsebody->as<AstStatBlock>())
elseBlock->hasEnd = hasEnd;
}
else
thenbody->hasEnd = hasEnd;
}
} }
return allocator.alloc<AstStatIf>(Location(start, end), cond, thenbody, elsebody, thenLocation, elseLocation, hasEnd); return allocator.alloc<AstStatIf>(Location(start, end), cond, thenbody, elsebody, thenLocation, elseLocation, DEPRECATED_hasEnd);
} }
// while exp do block end // while exp do block end
@ -426,6 +443,8 @@ AstStat* Parser::parseWhile()
Location end = lexer.current().location; Location end = lexer.current().location;
bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo);
if (FFlag::LuauClipExtraHasEndProps)
body->hasEnd = hasEnd;
return allocator.alloc<AstStatWhile>(Location(start, end), cond, body, hasDo, matchDo.location, hasEnd); return allocator.alloc<AstStatWhile>(Location(start, end), cond, body, hasDo, matchDo.location, hasEnd);
} }
@ -447,6 +466,8 @@ AstStat* Parser::parseRepeat()
functionStack.back().loopDepth--; functionStack.back().loopDepth--;
bool hasUntil = expectMatchEndAndConsume(Lexeme::ReservedUntil, matchRepeat); bool hasUntil = expectMatchEndAndConsume(Lexeme::ReservedUntil, matchRepeat);
if (FFlag::LuauClipExtraHasEndProps)
body->hasEnd = hasUntil;
AstExpr* cond = parseExpr(); AstExpr* cond = parseExpr();
@ -543,6 +564,8 @@ AstStat* Parser::parseFor()
Location end = lexer.current().location; Location end = lexer.current().location;
bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo);
if (FFlag::LuauClipExtraHasEndProps)
body->hasEnd = hasEnd;
return allocator.alloc<AstStatFor>(Location(start, end), var, from, to, step, body, hasDo, matchDo.location, hasEnd); return allocator.alloc<AstStatFor>(Location(start, end), var, from, to, step, body, hasDo, matchDo.location, hasEnd);
} }
@ -585,6 +608,8 @@ AstStat* Parser::parseFor()
Location end = lexer.current().location; Location end = lexer.current().location;
bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo); bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo);
if (FFlag::LuauClipExtraHasEndProps)
body->hasEnd = hasEnd;
return allocator.alloc<AstStatForIn>( return allocator.alloc<AstStatForIn>(
Location(start, end), copy(vars), copy(values), body, hasIn, inLocation, hasDo, matchDo.location, hasEnd); Location(start, end), copy(vars), copy(values), body, hasIn, inLocation, hasDo, matchDo.location, hasEnd);
@ -1074,6 +1099,8 @@ std::pair<AstExprFunction*, AstLocal*> Parser::parseFunctionBody(
Location end = lexer.current().location; Location end = lexer.current().location;
bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchFunction); bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchFunction);
if (FFlag::LuauClipExtraHasEndProps)
body->hasEnd = hasEnd;
return {allocator.alloc<AstExprFunction>(Location(start, end), generics, genericPacks, self, vars, vararg, varargLocation, body, return {allocator.alloc<AstExprFunction>(Location(start, end), generics, genericPacks, self, vars, vararg, varargLocation, body,
functionStack.size(), debugname, typelist, varargAnnotation, hasEnd, argLocation), functionStack.size(), debugname, typelist, varargAnnotation, hasEnd, argLocation),

View file

@ -165,11 +165,14 @@ static bool traverseDirectoryRec(const std::string& path, const std::function<vo
{ {
joinPaths(buf, path.c_str(), data.d_name); joinPaths(buf, path.c_str(), data.d_name);
int type = data.d_type; #if defined(DTTOIF)
int mode = -1; mode_t mode = DTTOIF(data.d_type);
#else
mode_t mode = 0;
#endif
// we need to stat DT_UNKNOWN to be able to tell the type // we need to stat an UNKNOWN to be able to tell the type
if (type == DT_UNKNOWN) if ((mode & S_IFMT) == 0)
{ {
struct stat st = {}; struct stat st = {};
#ifdef _ATFILE_SOURCE #ifdef _ATFILE_SOURCE
@ -181,15 +184,15 @@ static bool traverseDirectoryRec(const std::string& path, const std::function<vo
mode = st.st_mode; mode = st.st_mode;
} }
if (type == DT_DIR || mode == S_IFDIR) if (mode == S_IFDIR)
{ {
traverseDirectoryRec(buf, callback); traverseDirectoryRec(buf, callback);
} }
else if (type == DT_REG || mode == S_IFREG) else if (mode == S_IFREG)
{ {
callback(buf); callback(buf);
} }
else if (type == DT_LNK || mode == S_IFLNK) else if (mode == S_IFLNK)
{ {
// Skip symbolic links to avoid handling cycles // Skip symbolic links to avoid handling cycles
} }

View file

@ -20,7 +20,7 @@ struct IrFunction;
void updateUseCounts(IrFunction& function); void updateUseCounts(IrFunction& function);
void updateLastUseLocations(IrFunction& function); void updateLastUseLocations(IrFunction& function, const std::vector<uint32_t>& sortedBlocks);
uint32_t getNextInstUse(IrFunction& function, uint32_t targetInstIdx, uint32_t startInstIdx); uint32_t getNextInstUse(IrFunction& function, uint32_t targetInstIdx, uint32_t startInstIdx);

View file

@ -271,7 +271,7 @@ std::vector<uint32_t> getSortedBlockOrder(IrFunction& function);
// Returns first non-dead block that comes after block at index 'i' in the sorted blocks array // Returns first non-dead block that comes after block at index 'i' in the sorted blocks array
// 'dummy' block is returned if the end of array was reached // 'dummy' block is returned if the end of array was reached
IrBlock& getNextBlock(IrFunction& function, std::vector<uint32_t>& sortedBlocks, IrBlock& dummy, size_t i); IrBlock& getNextBlock(IrFunction& function, const std::vector<uint32_t>& sortedBlocks, IrBlock& dummy, size_t i);
} // namespace CodeGen } // namespace CodeGen
} // namespace Luau } // namespace Luau

View file

@ -48,10 +48,9 @@ inline void gatherFunctions(std::vector<Proto*>& results, Proto* proto, unsigned
} }
template<typename AssemblyBuilder, typename IrLowering> template<typename AssemblyBuilder, typename IrLowering>
inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction& function, int bytecodeid, AssemblyOptions options) inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction& function, const std::vector<uint32_t>& sortedBlocks, int bytecodeid,
AssemblyOptions options)
{ {
std::vector<uint32_t> sortedBlocks = getSortedBlockOrder(function);
// For each IR instruction that begins a bytecode instruction, which bytecode instruction is it? // For each IR instruction that begins a bytecode instruction, which bytecode instruction is it?
std::vector<uint32_t> bcLocations(function.instructions.size() + 1, ~0u); std::vector<uint32_t> bcLocations(function.instructions.size() + 1, ~0u);
@ -202,22 +201,22 @@ inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction&
return true; return true;
} }
inline bool lowerIr( inline bool lowerIr(X64::AssemblyBuilderX64& build, IrBuilder& ir, const std::vector<uint32_t>& sortedBlocks, ModuleHelpers& helpers, Proto* proto,
X64::AssemblyBuilderX64& build, IrBuilder& ir, ModuleHelpers& helpers, Proto* proto, AssemblyOptions options, LoweringStats* stats) AssemblyOptions options, LoweringStats* stats)
{ {
optimizeMemoryOperandsX64(ir.function); optimizeMemoryOperandsX64(ir.function);
X64::IrLoweringX64 lowering(build, helpers, ir.function, stats); X64::IrLoweringX64 lowering(build, helpers, ir.function, stats);
return lowerImpl(build, lowering, ir.function, proto->bytecodeid, options); return lowerImpl(build, lowering, ir.function, sortedBlocks, proto->bytecodeid, options);
} }
inline bool lowerIr( inline bool lowerIr(A64::AssemblyBuilderA64& build, IrBuilder& ir, const std::vector<uint32_t>& sortedBlocks, ModuleHelpers& helpers, Proto* proto,
A64::AssemblyBuilderA64& build, IrBuilder& ir, ModuleHelpers& helpers, Proto* proto, AssemblyOptions options, LoweringStats* stats) AssemblyOptions options, LoweringStats* stats)
{ {
A64::IrLoweringA64 lowering(build, helpers, ir.function, stats); A64::IrLoweringA64 lowering(build, helpers, ir.function, stats);
return lowerImpl(build, lowering, ir.function, proto->bytecodeid, options); return lowerImpl(build, lowering, ir.function, sortedBlocks, proto->bytecodeid, options);
} }
template<typename AssemblyBuilder> template<typename AssemblyBuilder>
@ -237,7 +236,12 @@ inline bool lowerFunction(IrBuilder& ir, AssemblyBuilder& build, ModuleHelpers&
createLinearBlocks(ir, useValueNumbering); createLinearBlocks(ir, useValueNumbering);
} }
return lowerIr(build, ir, helpers, proto, options, stats); std::vector<uint32_t> sortedBlocks = getSortedBlockOrder(ir.function);
// In order to allocate registers during lowering, we need to know where instruction results are last used
updateLastUseLocations(ir.function, sortedBlocks);
return lowerIr(build, ir, sortedBlocks, helpers, proto, options, stats);
} }
} // namespace CodeGen } // namespace CodeGen

View file

@ -54,31 +54,47 @@ void updateUseCounts(IrFunction& function)
} }
} }
void updateLastUseLocations(IrFunction& function) void updateLastUseLocations(IrFunction& function, const std::vector<uint32_t>& sortedBlocks)
{ {
std::vector<IrInst>& instructions = function.instructions; std::vector<IrInst>& instructions = function.instructions;
#if defined(LUAU_ASSERTENABLED)
// Last use assignements should be called only once
for (IrInst& inst : instructions) for (IrInst& inst : instructions)
inst.lastUse = 0; LUAU_ASSERT(inst.lastUse == 0);
#endif
for (size_t instIdx = 0; instIdx < instructions.size(); ++instIdx) for (size_t i = 0; i < sortedBlocks.size(); ++i)
{ {
IrInst& inst = instructions[instIdx]; uint32_t blockIndex = sortedBlocks[i];
IrBlock& block = function.blocks[blockIndex];
auto checkOp = [&](IrOp op) { if (block.kind == IrBlockKind::Dead)
if (op.kind == IrOpKind::Inst)
instructions[op.index].lastUse = uint32_t(instIdx);
};
if (isPseudo(inst.cmd))
continue; continue;
checkOp(inst.a); LUAU_ASSERT(block.start != ~0u);
checkOp(inst.b); LUAU_ASSERT(block.finish != ~0u);
checkOp(inst.c);
checkOp(inst.d); for (uint32_t instIdx = block.start; instIdx <= block.finish; instIdx++)
checkOp(inst.e); {
checkOp(inst.f); LUAU_ASSERT(instIdx < function.instructions.size());
IrInst& inst = instructions[instIdx];
auto checkOp = [&](IrOp op) {
if (op.kind == IrOpKind::Inst)
instructions[op.index].lastUse = uint32_t(instIdx);
};
if (isPseudo(inst.cmd))
continue;
checkOp(inst.a);
checkOp(inst.b);
checkOp(inst.c);
checkOp(inst.d);
checkOp(inst.e);
checkOp(inst.f);
}
} }
} }

View file

@ -249,9 +249,6 @@ IrLoweringA64::IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers,
, valueTracker(function) , valueTracker(function)
, exitHandlerMap(~0u) , exitHandlerMap(~0u)
{ {
// In order to allocate registers during lowering, we need to know where instruction results are last used
updateLastUseLocations(function);
valueTracker.setRestoreCallack(this, [](void* context, IrInst& inst) { valueTracker.setRestoreCallack(this, [](void* context, IrInst& inst) {
IrLoweringA64* self = static_cast<IrLoweringA64*>(context); IrLoweringA64* self = static_cast<IrLoweringA64*>(context);
self->regs.restoreReg(self->build, inst); self->regs.restoreReg(self->build, inst);
@ -2167,6 +2164,8 @@ AddressA64 IrLoweringA64::tempAddr(IrOp op, int offset)
{ {
// This is needed to tighten the bounds checks in the VmConst case below // This is needed to tighten the bounds checks in the VmConst case below
LUAU_ASSERT(offset % 4 == 0); LUAU_ASSERT(offset % 4 == 0);
// Full encoded range is wider depending on the load size, but this assertion helps establish a smaller guaranteed working range [0..4096)
LUAU_ASSERT(offset >= 0 && unsigned(offset / 4) <= AssemblyBuilderA64::kMaxImmediate);
if (op.kind == IrOpKind::VmReg) if (op.kind == IrOpKind::VmReg)
return mem(rBase, vmRegOp(op) * sizeof(TValue) + offset); return mem(rBase, vmRegOp(op) * sizeof(TValue) + offset);

View file

@ -31,9 +31,6 @@ IrLoweringX64::IrLoweringX64(AssemblyBuilderX64& build, ModuleHelpers& helpers,
, valueTracker(function) , valueTracker(function)
, exitHandlerMap(~0u) , exitHandlerMap(~0u)
{ {
// In order to allocate registers during lowering, we need to know where instruction results are last used
updateLastUseLocations(function);
valueTracker.setRestoreCallack(&regs, [](void* context, IrInst& inst) { valueTracker.setRestoreCallack(&regs, [](void* context, IrInst& inst) {
((IrRegAllocX64*)context)->restore(inst, false); ((IrRegAllocX64*)context)->restore(inst, false);
}); });

View file

@ -886,7 +886,7 @@ std::vector<uint32_t> getSortedBlockOrder(IrFunction& function)
return sortedBlocks; return sortedBlocks;
} }
IrBlock& getNextBlock(IrFunction& function, std::vector<uint32_t>& sortedBlocks, IrBlock& dummy, size_t i) IrBlock& getNextBlock(IrFunction& function, const std::vector<uint32_t>& sortedBlocks, IrBlock& dummy, size_t i)
{ {
for (size_t j = i + 1; j < sortedBlocks.size(); ++j) for (size_t j = i + 1; j < sortedBlocks.size(); ++j)
{ {

View file

@ -30,7 +30,7 @@ LUAU_FASTFLAGVARIABLE(LuauCompileFenvNoBuiltinFold, false)
LUAU_FASTFLAGVARIABLE(LuauCompileTopCold, false) LUAU_FASTFLAGVARIABLE(LuauCompileTopCold, false)
LUAU_FASTFLAG(LuauFloorDivision) LUAU_FASTFLAG(LuauFloorDivision)
LUAU_FASTFLAGVARIABLE(LuauCompileFixContinueValidation, false) LUAU_FASTFLAGVARIABLE(LuauCompileFixContinueValidation2, false)
LUAU_FASTFLAGVARIABLE(LuauCompileContinueCloseUpvals, false) LUAU_FASTFLAGVARIABLE(LuauCompileContinueCloseUpvals, false)
@ -276,7 +276,7 @@ struct Compiler
f.upvals = upvals; f.upvals = upvals;
// record information for inlining // record information for inlining
if (options.optimizationLevel >= 2 && !func->vararg && !getfenvUsed && !setfenvUsed) if (options.optimizationLevel >= 2 && !func->vararg && !func->self && !getfenvUsed && !setfenvUsed)
{ {
f.canInline = true; f.canInline = true;
f.stackSize = stackSize; f.stackSize = stackSize;
@ -665,14 +665,6 @@ struct Compiler
else else
{ {
locstants[arg.local] = arg.value; locstants[arg.local] = arg.value;
if (FFlag::LuauCompileFixContinueValidation)
{
// Mark that optimization skipped allocation of this local
Local& l = locals[arg.local];
LUAU_ASSERT(!l.skipped);
l.skipped = true;
}
} }
} }
@ -721,23 +713,8 @@ struct Compiler
{ {
AstLocal* local = func->args.data[i]; AstLocal* local = func->args.data[i];
if (FFlag::LuauCompileFixContinueValidation) if (Constant* var = locstants.find(local))
{ var->type = Constant::Type_Unknown;
if (Constant* var = locstants.find(local); var && var->type != Constant::Type_Unknown)
{
var->type = Constant::Type_Unknown;
// Restore local allocation skip flag as well
Local& l = locals[local];
LUAU_ASSERT(l.skipped);
l.skipped = false;
}
}
else
{
if (Constant* var = locstants.find(local))
var->type = Constant::Type_Unknown;
}
} }
foldConstants(constants, variables, locstants, builtinsFold, builtinsFoldMathK, func->body); foldConstants(constants, variables, locstants, builtinsFold, builtinsFoldMathK, func->body);
@ -2510,12 +2487,18 @@ struct Compiler
return; return;
} }
AstStat* continueStatement = extractStatContinue(stat->thenbody); AstStatContinue* continueStatement = extractStatContinue(stat->thenbody);
// Optimization: body is a "continue" statement with no "else" => we can directly continue in "then" case // Optimization: body is a "continue" statement with no "else" => we can directly continue in "then" case
if (!stat->elsebody && continueStatement != nullptr && !areLocalsCaptured(loops.back().localOffsetContinue)) if (!stat->elsebody && continueStatement != nullptr && !areLocalsCaptured(loops.back().localOffsetContinue))
{ {
if (loops.back().untilCondition) if (FFlag::LuauCompileFixContinueValidation2)
{
// track continue statement for repeat..until validation (validateContinueUntil)
if (!loops.back().continueUsed)
loops.back().continueUsed = continueStatement;
}
else if (loops.back().untilCondition)
validateContinueUntil(continueStatement, loops.back().untilCondition); validateContinueUntil(continueStatement, loops.back().untilCondition);
// fallthrough = proceed with the loop body as usual // fallthrough = proceed with the loop body as usual
@ -2577,7 +2560,7 @@ struct Compiler
size_t oldJumps = loopJumps.size(); size_t oldJumps = loopJumps.size();
size_t oldLocals = localStack.size(); size_t oldLocals = localStack.size();
loops.push_back({oldLocals, oldLocals, nullptr}); loops.push_back({oldLocals, oldLocals, nullptr, nullptr});
hasLoops = true; hasLoops = true;
size_t loopLabel = bytecode.emitLabel(); size_t loopLabel = bytecode.emitLabel();
@ -2613,7 +2596,7 @@ struct Compiler
size_t oldJumps = loopJumps.size(); size_t oldJumps = loopJumps.size();
size_t oldLocals = localStack.size(); size_t oldLocals = localStack.size();
loops.push_back({oldLocals, oldLocals, stat->condition}); loops.push_back({oldLocals, oldLocals, stat->condition, nullptr});
hasLoops = true; hasLoops = true;
size_t loopLabel = bytecode.emitLabel(); size_t loopLabel = bytecode.emitLabel();
@ -2624,6 +2607,8 @@ struct Compiler
RegScope rs(this); RegScope rs(this);
bool continueValidated = false;
for (size_t i = 0; i < body->body.size; ++i) for (size_t i = 0; i < body->body.size; ++i)
{ {
compileStat(body->body.data[i]); compileStat(body->body.data[i]);
@ -2634,6 +2619,14 @@ struct Compiler
// expression that continue will jump to. // expression that continue will jump to.
if (FFlag::LuauCompileContinueCloseUpvals) if (FFlag::LuauCompileContinueCloseUpvals)
loops.back().localOffsetContinue = localStack.size(); loops.back().localOffsetContinue = localStack.size();
// if continue was called from this statement, then any local defined after this in the loop body should not be accessed by until condition
// it is sufficient to check this condition once, as if this holds for the first continue, it must hold for all subsequent continues.
if (FFlag::LuauCompileFixContinueValidation2 && loops.back().continueUsed && !continueValidated)
{
validateContinueUntil(loops.back().continueUsed, stat->condition, body, i + 1);
continueValidated = true;
}
} }
size_t contLabel = bytecode.emitLabel(); size_t contLabel = bytecode.emitLabel();
@ -2762,19 +2755,7 @@ struct Compiler
{ {
// Optimization: we don't need to allocate and assign const locals, since their uses will be constant-folded // Optimization: we don't need to allocate and assign const locals, since their uses will be constant-folded
if (options.optimizationLevel >= 1 && options.debugLevel <= 1 && areLocalsRedundant(stat)) if (options.optimizationLevel >= 1 && options.debugLevel <= 1 && areLocalsRedundant(stat))
{
if (FFlag::LuauCompileFixContinueValidation)
{
// Mark that optimization skipped allocation of this local
for (AstLocal* local : stat->vars)
{
Local& l = locals[local];
l.skipped = true;
}
}
return; return;
}
// Optimization: for 1-1 local assignments, we can reuse the register *if* neither local is mutated // Optimization: for 1-1 local assignments, we can reuse the register *if* neither local is mutated
if (options.optimizationLevel >= 1 && stat->vars.size == 1 && stat->values.size == 1) if (options.optimizationLevel >= 1 && stat->vars.size == 1 && stat->values.size == 1)
@ -2863,7 +2844,7 @@ struct Compiler
size_t oldLocals = localStack.size(); size_t oldLocals = localStack.size();
size_t oldJumps = loopJumps.size(); size_t oldJumps = loopJumps.size();
loops.push_back({oldLocals, oldLocals, nullptr}); loops.push_back({oldLocals, oldLocals, nullptr, nullptr});
for (int iv = 0; iv < tripCount; ++iv) for (int iv = 0; iv < tripCount; ++iv)
{ {
@ -2914,7 +2895,7 @@ struct Compiler
size_t oldLocals = localStack.size(); size_t oldLocals = localStack.size();
size_t oldJumps = loopJumps.size(); size_t oldJumps = loopJumps.size();
loops.push_back({oldLocals, oldLocals, nullptr}); loops.push_back({oldLocals, oldLocals, nullptr, nullptr});
hasLoops = true; hasLoops = true;
// register layout: limit, step, index // register layout: limit, step, index
@ -2979,7 +2960,7 @@ struct Compiler
size_t oldLocals = localStack.size(); size_t oldLocals = localStack.size();
size_t oldJumps = loopJumps.size(); size_t oldJumps = loopJumps.size();
loops.push_back({oldLocals, oldLocals, nullptr}); loops.push_back({oldLocals, oldLocals, nullptr, nullptr});
hasLoops = true; hasLoops = true;
// register layout: generator, state, index, variables... // register layout: generator, state, index, variables...
@ -3391,7 +3372,13 @@ struct Compiler
{ {
LUAU_ASSERT(!loops.empty()); LUAU_ASSERT(!loops.empty());
if (loops.back().untilCondition) if (FFlag::LuauCompileFixContinueValidation2)
{
// track continue statement for repeat..until validation (validateContinueUntil)
if (!loops.back().continueUsed)
loops.back().continueUsed = stat;
}
else if (loops.back().untilCondition)
validateContinueUntil(stat, loops.back().untilCondition); validateContinueUntil(stat, loops.back().untilCondition);
// before continuing, we need to close all local variables that were captured in closures since loop start // before continuing, we need to close all local variables that were captured in closures since loop start
@ -3477,6 +3464,7 @@ struct Compiler
void validateContinueUntil(AstStat* cont, AstExpr* condition) void validateContinueUntil(AstStat* cont, AstExpr* condition)
{ {
LUAU_ASSERT(!FFlag::LuauCompileFixContinueValidation2);
UndefinedLocalVisitor visitor(this); UndefinedLocalVisitor visitor(this);
condition->visit(&visitor); condition->visit(&visitor);
@ -3486,6 +3474,32 @@ struct Compiler
visitor.undef->name.value, cont->location.begin.line + 1); visitor.undef->name.value, cont->location.begin.line + 1);
} }
void validateContinueUntil(AstStat* cont, AstExpr* condition, AstStatBlock* body, size_t start)
{
LUAU_ASSERT(FFlag::LuauCompileFixContinueValidation2);
UndefinedLocalVisitor visitor(this);
for (size_t i = start; i < body->body.size; ++i)
{
if (AstStatLocal* stat = body->body.data[i]->as<AstStatLocal>())
{
for (AstLocal* local : stat->vars)
visitor.locals.insert(local);
}
else if (AstStatLocalFunction* stat = body->body.data[i]->as<AstStatLocalFunction>())
{
visitor.locals.insert(stat->name);
}
}
condition->visit(&visitor);
if (visitor.undef)
CompileError::raise(condition->location,
"Local %s used in the repeat..until condition is undefined because continue statement on line %d jumps over it",
visitor.undef->name.value, cont->location.begin.line + 1);
}
void gatherConstUpvals(AstExprFunction* func) void gatherConstUpvals(AstExprFunction* func)
{ {
ConstUpvalueVisitor visitor(this); ConstUpvalueVisitor visitor(this);
@ -3702,18 +3716,24 @@ struct Compiler
UndefinedLocalVisitor(Compiler* self) UndefinedLocalVisitor(Compiler* self)
: self(self) : self(self)
, undef(nullptr) , undef(nullptr)
, locals(nullptr)
{ {
} }
void check(AstLocal* local) void check(AstLocal* local)
{ {
Local& l = self->locals[local]; if (FFlag::LuauCompileFixContinueValidation2)
{
if (!undef && locals.contains(local))
undef = local;
}
else
{
Local& l = self->locals[local];
if (FFlag::LuauCompileFixContinueValidation && l.skipped) if (!l.allocated && !undef)
return; undef = local;
}
if (!l.allocated && !undef)
undef = local;
} }
bool visit(AstExprLocal* node) override bool visit(AstExprLocal* node) override
@ -3742,6 +3762,7 @@ struct Compiler
Compiler* self; Compiler* self;
AstLocal* undef; AstLocal* undef;
DenseHashSet<AstLocal*> locals;
}; };
struct ConstUpvalueVisitor : AstVisitor struct ConstUpvalueVisitor : AstVisitor
@ -3837,7 +3858,6 @@ struct Compiler
uint8_t reg = 0; uint8_t reg = 0;
bool allocated = false; bool allocated = false;
bool captured = false; bool captured = false;
bool skipped = false;
uint32_t debugpc = 0; uint32_t debugpc = 0;
}; };
@ -3858,7 +3878,10 @@ struct Compiler
size_t localOffset; size_t localOffset;
size_t localOffsetContinue; size_t localOffsetContinue;
// TODO: Remove with LuauCompileFixContinueValidation2
AstExpr* untilCondition; AstExpr* untilCondition;
AstStatContinue* continueUsed;
}; };
struct InlineArg struct InlineArg

View file

@ -196,6 +196,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/Transpiler.h Analysis/include/Luau/Transpiler.h
Analysis/include/Luau/TxnLog.h Analysis/include/Luau/TxnLog.h
Analysis/include/Luau/Type.h Analysis/include/Luau/Type.h
Analysis/include/Luau/TypePairHash.h
Analysis/include/Luau/TypeArena.h Analysis/include/Luau/TypeArena.h
Analysis/include/Luau/TypeAttach.h Analysis/include/Luau/TypeAttach.h
Analysis/include/Luau/TypeChecker2.h Analysis/include/Luau/TypeChecker2.h
@ -441,6 +442,7 @@ if(TARGET Luau.UnitTest)
tests/TypeInfer.test.cpp tests/TypeInfer.test.cpp
tests/TypeInfer.tryUnify.test.cpp tests/TypeInfer.tryUnify.test.cpp
tests/TypeInfer.typePacks.cpp tests/TypeInfer.typePacks.cpp
tests/TypeInfer.typestates.test.cpp
tests/TypeInfer.unionTypes.test.cpp tests/TypeInfer.unionTypes.test.cpp
tests/TypeInfer.unknownnever.test.cpp tests/TypeInfer.unknownnever.test.cpp
tests/TypePack.test.cpp tests/TypePack.test.cpp

View file

@ -16,6 +16,8 @@ struct JsonEncoderFixture
Allocator allocator; Allocator allocator;
AstNameTable names{allocator}; AstNameTable names{allocator};
ScopedFastFlag sff{"LuauClipExtraHasEndProps", true};
ParseResult parse(std::string_view src) ParseResult parse(std::string_view src)
{ {
ParseOptions opts; ParseOptions opts;
@ -91,6 +93,8 @@ TEST_CASE("basic_escaping")
TEST_CASE("encode_AstStatBlock") TEST_CASE("encode_AstStatBlock")
{ {
ScopedFastFlag sff{"LuauClipExtraHasEndProps", true};
AstLocal astlocal{AstName{"a_local"}, Location(), nullptr, 0, 0, nullptr}; AstLocal astlocal{AstName{"a_local"}, Location(), nullptr, 0, 0, nullptr};
AstLocal* astlocalarray[] = {&astlocal}; AstLocal* astlocalarray[] = {&astlocal};
@ -103,9 +107,9 @@ TEST_CASE("encode_AstStatBlock")
AstStatBlock block{Location(), bodyArray}; AstStatBlock block{Location(), bodyArray};
CHECK_EQ( CHECK(
(R"({"type":"AstStatBlock","location":"0,0 - 0,0","body":[{"type":"AstStatLocal","location":"0,0 - 0,0","vars":[{"luauType":null,"name":"a_local","type":"AstLocal","location":"0,0 - 0,0"}],"values":[]}]})"), toJson(&block) ==
toJson(&block)); (R"({"type":"AstStatBlock","location":"0,0 - 0,0","hasEnd":true,"body":[{"type":"AstStatLocal","location":"0,0 - 0,0","vars":[{"luauType":null,"name":"a_local","type":"AstLocal","location":"0,0 - 0,0"}],"values":[]}]})"));
} }
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_tables") TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_tables")
@ -123,7 +127,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_tables")
CHECK( CHECK(
json == json ==
R"({"type":"AstStatBlock","location":"0,0 - 6,4","body":[{"type":"AstStatLocal","location":"1,8 - 5,9","vars":[{"luauType":{"type":"AstTypeTable","location":"1,17 - 3,9","props":[{"name":"foo","type":"AstTableProp","location":"2,12 - 2,15","propType":{"type":"AstTypeReference","location":"2,17 - 2,23","name":"number","nameLocation":"2,17 - 2,23","parameters":[]}}],"indexer":null},"name":"x","type":"AstLocal","location":"1,14 - 1,15"}],"values":[{"type":"AstExprTable","location":"3,12 - 5,9","items":[{"type":"AstExprTableItem","kind":"record","key":{"type":"AstExprConstantString","location":"4,12 - 4,15","value":"foo"},"value":{"type":"AstExprConstantNumber","location":"4,18 - 4,21","value":123}}]}]}]})"); R"({"type":"AstStatBlock","location":"0,0 - 6,4","hasEnd":true,"body":[{"type":"AstStatLocal","location":"1,8 - 5,9","vars":[{"luauType":{"type":"AstTypeTable","location":"1,17 - 3,9","props":[{"name":"foo","type":"AstTableProp","location":"2,12 - 2,15","propType":{"type":"AstTypeReference","location":"2,17 - 2,23","name":"number","nameLocation":"2,17 - 2,23","parameters":[]}}],"indexer":null},"name":"x","type":"AstLocal","location":"1,14 - 1,15"}],"values":[{"type":"AstExprTable","location":"3,12 - 5,9","items":[{"type":"AstExprTableItem","kind":"record","key":{"type":"AstExprConstantString","location":"4,12 - 4,15","value":"foo"},"value":{"type":"AstExprConstantNumber","location":"4,18 - 4,21","value":123}}]}]}]})");
} }
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_table_array") TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_table_array")
@ -135,7 +139,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_table_array")
CHECK( CHECK(
json == json ==
R"({"type":"AstStatBlock","location":"0,0 - 0,17","body":[{"type":"AstStatTypeAlias","location":"0,0 - 0,17","name":"X","generics":[],"genericPacks":[],"type":{"type":"AstTypeTable","location":"0,9 - 0,17","props":[],"indexer":{"location":"0,10 - 0,16","indexType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"number","nameLocation":"0,10 - 0,16","parameters":[]},"resultType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"string","nameLocation":"0,10 - 0,16","parameters":[]}}},"exported":false}]})"); R"({"type":"AstStatBlock","location":"0,0 - 0,17","hasEnd":true,"body":[{"type":"AstStatTypeAlias","location":"0,0 - 0,17","name":"X","generics":[],"genericPacks":[],"type":{"type":"AstTypeTable","location":"0,9 - 0,17","props":[],"indexer":{"location":"0,10 - 0,16","indexType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"number","nameLocation":"0,10 - 0,16","parameters":[]},"resultType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"string","nameLocation":"0,10 - 0,16","parameters":[]}}},"exported":false}]})");
} }
TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_table_indexer") TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_table_indexer")
@ -147,7 +151,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_table_indexer")
CHECK( CHECK(
json == json ==
R"({"type":"AstStatBlock","location":"0,0 - 0,17","body":[{"type":"AstStatTypeAlias","location":"0,0 - 0,17","name":"X","generics":[],"genericPacks":[],"type":{"type":"AstTypeTable","location":"0,9 - 0,17","props":[],"indexer":{"location":"0,10 - 0,16","indexType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"number","nameLocation":"0,10 - 0,16","parameters":[]},"resultType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"string","nameLocation":"0,10 - 0,16","parameters":[]}}},"exported":false}]})"); R"({"type":"AstStatBlock","location":"0,0 - 0,17","hasEnd":true,"body":[{"type":"AstStatTypeAlias","location":"0,0 - 0,17","name":"X","generics":[],"genericPacks":[],"type":{"type":"AstTypeTable","location":"0,9 - 0,17","props":[],"indexer":{"location":"0,10 - 0,16","indexType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"number","nameLocation":"0,10 - 0,16","parameters":[]},"resultType":{"type":"AstTypeReference","location":"0,10 - 0,16","name":"string","nameLocation":"0,10 - 0,16","parameters":[]}}},"exported":false}]})");
} }
TEST_CASE("encode_AstExprGroup") TEST_CASE("encode_AstExprGroup")
@ -243,7 +247,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprFunction")
AstExpr* expr = expectParseExpr("function (a) return a end"); AstExpr* expr = expectParseExpr("function (a) return a end");
std::string_view expected = std::string_view expected =
R"({"type":"AstExprFunction","location":"0,4 - 0,29","generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"a","type":"AstLocal","location":"0,14 - 0,15"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,16 - 0,26","body":[{"type":"AstStatReturn","location":"0,17 - 0,25","list":[{"type":"AstExprLocal","location":"0,24 - 0,25","local":{"luauType":null,"name":"a","type":"AstLocal","location":"0,14 - 0,15"}}]}]},"functionDepth":1,"debugname":"","hasEnd":true})"; R"({"type":"AstExprFunction","location":"0,4 - 0,29","generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"a","type":"AstLocal","location":"0,14 - 0,15"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,16 - 0,26","hasEnd":true,"body":[{"type":"AstStatReturn","location":"0,17 - 0,25","list":[{"type":"AstExprLocal","location":"0,24 - 0,25","local":{"luauType":null,"name":"a","type":"AstLocal","location":"0,14 - 0,15"}}]}]},"functionDepth":1,"debugname":""})";
CHECK(toJson(expr) == expected); CHECK(toJson(expr) == expected);
} }
@ -311,7 +315,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatIf")
AstStat* statement = expectParseStatement("if true then else end"); AstStat* statement = expectParseStatement("if true then else end");
std::string_view expected = std::string_view expected =
R"({"type":"AstStatIf","location":"0,0 - 0,21","condition":{"type":"AstExprConstantBool","location":"0,3 - 0,7","value":true},"thenbody":{"type":"AstStatBlock","location":"0,12 - 0,13","body":[]},"elsebody":{"type":"AstStatBlock","location":"0,17 - 0,18","body":[]},"hasThen":true,"hasEnd":true})"; R"({"type":"AstStatIf","location":"0,0 - 0,21","condition":{"type":"AstExprConstantBool","location":"0,3 - 0,7","value":true},"thenbody":{"type":"AstStatBlock","location":"0,12 - 0,13","hasEnd":true,"body":[]},"elsebody":{"type":"AstStatBlock","location":"0,17 - 0,18","hasEnd":true,"body":[]},"hasThen":true})";
CHECK(toJson(statement) == expected); CHECK(toJson(statement) == expected);
} }
@ -321,7 +325,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatWhile")
AstStat* statement = expectParseStatement("while true do end"); AstStat* statement = expectParseStatement("while true do end");
std::string_view expected = std::string_view expected =
R"({"type":"AstStatWhile","location":"0,0 - 0,17","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,14","body":[]},"hasDo":true,"hasEnd":true})"; R"({"type":"AstStatWhile","location":"0,0 - 0,17","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,14","hasEnd":true,"body":[]},"hasDo":true})";
CHECK(toJson(statement) == expected); CHECK(toJson(statement) == expected);
} }
@ -331,7 +335,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatRepeat")
AstStat* statement = expectParseStatement("repeat until true"); AstStat* statement = expectParseStatement("repeat until true");
std::string_view expected = std::string_view expected =
R"({"type":"AstStatRepeat","location":"0,0 - 0,17","condition":{"type":"AstExprConstantBool","location":"0,13 - 0,17","value":true},"body":{"type":"AstStatBlock","location":"0,6 - 0,7","body":[]},"hasUntil":true})"; R"({"type":"AstStatRepeat","location":"0,0 - 0,17","condition":{"type":"AstExprConstantBool","location":"0,13 - 0,17","value":true},"body":{"type":"AstStatBlock","location":"0,6 - 0,7","hasEnd":true,"body":[]}})";
CHECK(toJson(statement) == expected); CHECK(toJson(statement) == expected);
} }
@ -341,7 +345,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatBreak")
AstStat* statement = expectParseStatement("while true do break end"); AstStat* statement = expectParseStatement("while true do break end");
std::string_view expected = std::string_view expected =
R"({"type":"AstStatWhile","location":"0,0 - 0,23","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,20","body":[{"type":"AstStatBreak","location":"0,14 - 0,19"}]},"hasDo":true,"hasEnd":true})"; R"({"type":"AstStatWhile","location":"0,0 - 0,23","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,20","hasEnd":true,"body":[{"type":"AstStatBreak","location":"0,14 - 0,19"}]},"hasDo":true})";
CHECK(toJson(statement) == expected); CHECK(toJson(statement) == expected);
} }
@ -351,7 +355,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatContinue")
AstStat* statement = expectParseStatement("while true do continue end"); AstStat* statement = expectParseStatement("while true do continue end");
std::string_view expected = std::string_view expected =
R"({"type":"AstStatWhile","location":"0,0 - 0,26","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,23","body":[{"type":"AstStatContinue","location":"0,14 - 0,22"}]},"hasDo":true,"hasEnd":true})"; R"({"type":"AstStatWhile","location":"0,0 - 0,26","condition":{"type":"AstExprConstantBool","location":"0,6 - 0,10","value":true},"body":{"type":"AstStatBlock","location":"0,13 - 0,23","hasEnd":true,"body":[{"type":"AstStatContinue","location":"0,14 - 0,22"}]},"hasDo":true})";
CHECK(toJson(statement) == expected); CHECK(toJson(statement) == expected);
} }
@ -361,7 +365,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatFor")
AstStat* statement = expectParseStatement("for a=0,1 do end"); AstStat* statement = expectParseStatement("for a=0,1 do end");
std::string_view expected = std::string_view expected =
R"({"type":"AstStatFor","location":"0,0 - 0,16","var":{"luauType":null,"name":"a","type":"AstLocal","location":"0,4 - 0,5"},"from":{"type":"AstExprConstantNumber","location":"0,6 - 0,7","value":0},"to":{"type":"AstExprConstantNumber","location":"0,8 - 0,9","value":1},"body":{"type":"AstStatBlock","location":"0,12 - 0,13","body":[]},"hasDo":true,"hasEnd":true})"; R"({"type":"AstStatFor","location":"0,0 - 0,16","var":{"luauType":null,"name":"a","type":"AstLocal","location":"0,4 - 0,5"},"from":{"type":"AstExprConstantNumber","location":"0,6 - 0,7","value":0},"to":{"type":"AstExprConstantNumber","location":"0,8 - 0,9","value":1},"body":{"type":"AstStatBlock","location":"0,12 - 0,13","hasEnd":true,"body":[]},"hasDo":true})";
CHECK(toJson(statement) == expected); CHECK(toJson(statement) == expected);
} }
@ -371,7 +375,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatForIn")
AstStat* statement = expectParseStatement("for a in b do end"); AstStat* statement = expectParseStatement("for a in b do end");
std::string_view expected = std::string_view expected =
R"({"type":"AstStatForIn","location":"0,0 - 0,17","vars":[{"luauType":null,"name":"a","type":"AstLocal","location":"0,4 - 0,5"}],"values":[{"type":"AstExprGlobal","location":"0,9 - 0,10","global":"b"}],"body":{"type":"AstStatBlock","location":"0,13 - 0,14","body":[]},"hasIn":true,"hasDo":true,"hasEnd":true})"; R"({"type":"AstStatForIn","location":"0,0 - 0,17","vars":[{"luauType":null,"name":"a","type":"AstLocal","location":"0,4 - 0,5"}],"values":[{"type":"AstExprGlobal","location":"0,9 - 0,10","global":"b"}],"body":{"type":"AstStatBlock","location":"0,13 - 0,14","hasEnd":true,"body":[]},"hasIn":true,"hasDo":true})";
CHECK(toJson(statement) == expected); CHECK(toJson(statement) == expected);
} }
@ -391,7 +395,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatLocalFunction")
AstStat* statement = expectParseStatement("local function a(b) return end"); AstStat* statement = expectParseStatement("local function a(b) return end");
std::string_view expected = std::string_view expected =
R"({"type":"AstStatLocalFunction","location":"0,0 - 0,30","name":{"luauType":null,"name":"a","type":"AstLocal","location":"0,15 - 0,16"},"func":{"type":"AstExprFunction","location":"0,0 - 0,30","generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"b","type":"AstLocal","location":"0,17 - 0,18"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,19 - 0,27","body":[{"type":"AstStatReturn","location":"0,20 - 0,26","list":[]}]},"functionDepth":1,"debugname":"a","hasEnd":true}})"; R"({"type":"AstStatLocalFunction","location":"0,0 - 0,30","name":{"luauType":null,"name":"a","type":"AstLocal","location":"0,15 - 0,16"},"func":{"type":"AstExprFunction","location":"0,0 - 0,30","generics":[],"genericPacks":[],"args":[{"luauType":null,"name":"b","type":"AstLocal","location":"0,17 - 0,18"}],"vararg":false,"varargLocation":"0,0 - 0,0","body":{"type":"AstStatBlock","location":"0,19 - 0,27","hasEnd":true,"body":[{"type":"AstStatReturn","location":"0,20 - 0,26","list":[]}]},"functionDepth":1,"debugname":"a"}})";
CHECK(toJson(statement) == expected); CHECK(toJson(statement) == expected);
} }

View file

@ -1911,7 +1911,7 @@ RETURN R0 0
TEST_CASE("LoopContinueIgnoresImplicitConstant") TEST_CASE("LoopContinueIgnoresImplicitConstant")
{ {
ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true}; ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true};
// this used to crash the compiler :( // this used to crash the compiler :(
CHECK_EQ("\n" + compileFunction0(R"( CHECK_EQ("\n" + compileFunction0(R"(
@ -1928,7 +1928,7 @@ RETURN R0 0
TEST_CASE("LoopContinueIgnoresExplicitConstant") TEST_CASE("LoopContinueIgnoresExplicitConstant")
{ {
ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true}; ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true};
// Constants do not allocate locals and 'continue' validation should skip them if their lifetime already started // Constants do not allocate locals and 'continue' validation should skip them if their lifetime already started
CHECK_EQ("\n" + compileFunction0(R"( CHECK_EQ("\n" + compileFunction0(R"(
@ -1945,7 +1945,7 @@ RETURN R0 0
TEST_CASE("LoopContinueRespectsExplicitConstant") TEST_CASE("LoopContinueRespectsExplicitConstant")
{ {
ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true}; ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true};
// If local lifetime hasn't started, even if it's a constant that will not receive an allocation, it cannot be jumped over // If local lifetime hasn't started, even if it's a constant that will not receive an allocation, it cannot be jumped over
try try
@ -1971,7 +1971,7 @@ until c
TEST_CASE("LoopContinueIgnoresImplicitConstantAfterInline") TEST_CASE("LoopContinueIgnoresImplicitConstantAfterInline")
{ {
ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true}; ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true};
// Inlining might also replace some locals with constants instead of allocating them // Inlining might also replace some locals with constants instead of allocating them
CHECK_EQ("\n" + compileFunction(R"( CHECK_EQ("\n" + compileFunction(R"(
@ -1994,6 +1994,36 @@ RETURN R0 0
)"); )");
} }
TEST_CASE("LoopContinueCorrectlyHandlesImplicitConstantAfterUnroll")
{
ScopedFastFlag sff{"LuauCompileFixContinueValidation2", true};
ScopedFastInt sfi("LuauCompileLoopUnrollThreshold", 200);
// access to implicit constant that depends on the unrolled loop constant is still invalid even though we can constant-propagate it
try
{
compileFunction(R"(
for i = 1, 2 do
s()
repeat
if i == 2 then
continue
end
local x = i == 1 or a
until f(x)
end
)", 0, 2);
CHECK(!"Expected CompileError");
}
catch (Luau::CompileError& e)
{
CHECK_EQ(e.getLocation().begin.line + 1, 9);
CHECK_EQ(
std::string(e.what()), "Local x used in the repeat..until condition is undefined because continue statement on line 6 jumps over it");
}
}
TEST_CASE("LoopContinueUntilCapture") TEST_CASE("LoopContinueUntilCapture")
{ {
ScopedFastFlag sff("LuauCompileContinueCloseUpvals", true); ScopedFastFlag sff("LuauCompileContinueCloseUpvals", true);

View file

@ -757,8 +757,8 @@ TEST_CASE_FIXTURE(DifferFixture, "singleton_string")
TEST_CASE_FIXTURE(DifferFixtureWithBuiltins, "negation") TEST_CASE_FIXTURE(DifferFixtureWithBuiltins, "negation")
{ {
// Old solver does not correctly refine test types if (!FFlag::DebugLuauDeferredConstraintResolution)
ScopedFastFlag sff{"DebugLuauDeferredConstraintResolution", true}; return;
CheckResult result = check(R"( CheckResult result = check(R"(
local bar: { x: { y: unknown }} local bar: { x: { y: unknown }}

View file

@ -174,7 +174,8 @@ AstStatBlock* Fixture::parse(const std::string& source, const ParseOptions& pars
{ {
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
ModulePtr module = Luau::check(*sourceModule, {}, builtinTypes, NotNull{&ice}, NotNull{&moduleResolver}, NotNull{&fileResolver}, Mode mode = sourceModule->mode ? *sourceModule->mode : Mode::Strict;
ModulePtr module = Luau::check(*sourceModule, mode, {}, builtinTypes, NotNull{&ice}, NotNull{&moduleResolver}, NotNull{&fileResolver},
frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, frontend.options, {}); frontend.globals.globalScope, /*prepareModuleScope*/ nullptr, frontend.options, {});
Luau::lint(sourceModule->root, *sourceModule->names, frontend.globals.globalScope, module.get(), sourceModule->hotcomments, {}); Luau::lint(sourceModule->root, *sourceModule->names, frontend.globals.globalScope, module.get(), sourceModule->hotcomments, {});
@ -194,7 +195,7 @@ AstStatBlock* Fixture::parse(const std::string& source, const ParseOptions& pars
return result.root; return result.root;
} }
CheckResult Fixture::check(Mode mode, std::string source) CheckResult Fixture::check(Mode mode, const std::string& source)
{ {
ModuleName mm = fromString(mainModuleName); ModuleName mm = fromString(mainModuleName);
configResolver.defaultConfig.mode = mode; configResolver.defaultConfig.mode = mode;

View file

@ -65,7 +65,7 @@ struct Fixture
// Throws Luau::ParseErrors if the parse fails. // Throws Luau::ParseErrors if the parse fails.
AstStatBlock* parse(const std::string& source, const ParseOptions& parseOptions = {}); AstStatBlock* parse(const std::string& source, const ParseOptions& parseOptions = {});
CheckResult check(Mode mode, std::string source); CheckResult check(Mode mode, const std::string& source);
CheckResult check(const std::string& source); CheckResult check(const std::string& source);
LintResult lint(const std::string& source, const std::optional<LintOptions>& lintOptions = {}); LintResult lint(const std::string& source, const std::optional<LintOptions>& lintOptions = {});

View file

@ -1502,6 +1502,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "DeprecatedApiUntyped")
} }
LintResult result = lint(R"( LintResult result = lint(R"(
-- TODO
return function () return function ()
print(table.getn({})) print(table.getn({}))
table.foreach({}, function() end) table.foreach({}, function() end)
@ -1514,6 +1515,35 @@ end
CHECK_EQ(result.warnings[1].text, "Member 'table.foreach' is deprecated"); CHECK_EQ(result.warnings[1].text, "Member 'table.foreach' is deprecated");
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "DeprecatedApiFenv")
{
ScopedFastFlag sff("LuauLintDeprecatedFenv", true);
LintResult result = lint(R"(
local f, g, h = ...
getfenv(1)
getfenv(f :: () -> ())
getfenv(g :: number)
getfenv(h :: any)
setfenv(1, {})
setfenv(f :: () -> (), {})
setfenv(g :: number, {})
setfenv(h :: any, {})
)");
REQUIRE(4 == result.warnings.size());
CHECK_EQ(result.warnings[0].text, "Function 'getfenv' is deprecated; consider using 'debug.info' instead");
CHECK_EQ(result.warnings[0].location.begin.line + 1, 4);
CHECK_EQ(result.warnings[1].text, "Function 'getfenv' is deprecated; consider using 'debug.info' instead");
CHECK_EQ(result.warnings[1].location.begin.line + 1, 6);
CHECK_EQ(result.warnings[2].text, "Function 'setfenv' is deprecated");
CHECK_EQ(result.warnings[2].location.begin.line + 1, 9);
CHECK_EQ(result.warnings[3].text, "Function 'setfenv' is deprecated");
CHECK_EQ(result.warnings[3].location.begin.line + 1, 11);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "TableOperations") TEST_CASE_FIXTURE(BuiltinsFixture, "TableOperations")
{ {
LintResult result = lint(R"( LintResult result = lint(R"(
@ -1559,6 +1589,62 @@ table.create(42, {} :: {})
result.warnings[9].text, "table.create with a table literal will reuse the same object for all elements; consider using a for loop instead"); result.warnings[9].text, "table.create with a table literal will reuse the same object for all elements; consider using a for loop instead");
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "TableOperationsIndexer")
{
ScopedFastFlag sff("LuauLintTableIndexer", true);
LintResult result = lint(R"(
local t1 = {} -- ok: empty
local t2 = {1, 2} -- ok: array
local t3 = { a = 1, b = 2 } -- not ok: dictionary
local t4: {[number]: number} = {} -- ok: array
local t5: {[string]: number} = {} -- not ok: dictionary
local t6: typeof(setmetatable({1, 2}, {})) = {} -- ok: table with metatable
local t7: string = "hello" -- ok: string
local t8: {number} | {n: number} = {} -- ok: union
-- not ok
print(#t3)
print(#t5)
ipairs(t5)
-- disabled
-- ipairs(t3) adds indexer to t3, silencing error on #t3
-- ok
print(#t1)
print(#t2)
print(#t4)
print(#t6)
print(#t7)
print(#t8)
ipairs(t1)
ipairs(t2)
ipairs(t4)
ipairs(t6)
ipairs(t7)
ipairs(t8)
-- ok, subtle: text is a string here implicitly, but the type annotation isn't available
-- type checker assigns a type of generic table with the 'sub' member; we don't emit warnings on generic tables
-- to avoid generating a false positive here
function _impliedstring(element, text)
for i = 1, #text do
element:sendText(text:sub(i, i))
end
end
)");
REQUIRE(3 == result.warnings.size());
CHECK_EQ(result.warnings[0].location.begin.line + 1, 12);
CHECK_EQ(result.warnings[0].text, "Using '#' on a table without an array part is likely a bug");
CHECK_EQ(result.warnings[1].location.begin.line + 1, 13);
CHECK_EQ(result.warnings[1].text, "Using '#' on a table with string keys is likely a bug");
CHECK_EQ(result.warnings[2].location.begin.line + 1, 14);
CHECK_EQ(result.warnings[2].text, "Using 'ipairs' on a table with string keys is likely a bug");
}
TEST_CASE_FIXTURE(Fixture, "DuplicateConditions") TEST_CASE_FIXTURE(Fixture, "DuplicateConditions")
{ {
LintResult result = lint(R"( LintResult result = lint(R"(

View file

@ -15,102 +15,32 @@ using namespace Luau;
struct NonStrictTypeCheckerFixture : Fixture struct NonStrictTypeCheckerFixture : Fixture
{ {
ParseResult parse(std::string source) CheckResult checkNonStrict(const std::string& code)
{ {
ParseOptions opts; ScopedFastFlag flags[] = {
opts.allowDeclarationSyntax = true; {"LuauCheckedFunctionSyntax", true},
ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true}; };
return tryParse(source, opts);
return check(Mode::Nonstrict, code);
} }
std::string definitions = R"BUILTIN_SRC(
declare function @checked abs(n: number): number
abs("hi")
)BUILTIN_SRC";
}; };
TEST_SUITE_BEGIN("NonStrictTypeCheckerTest"); TEST_SUITE_BEGIN("NonStrictTypeCheckerTest");
TEST_CASE_FIXTURE(Fixture, "basic") TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "simple_non_strict")
{ {
Luau::checkNonStrict(builtinTypes, nullptr); auto res = checkNonStrict(R"BUILTIN_SRC(
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_top_level_checked_fn")
{
std::string src = R"BUILTIN_SRC(
declare function @checked abs(n: number): number declare function @checked abs(n: number): number
)BUILTIN_SRC"; abs("hi")
)BUILTIN_SRC");
ParseResult pr = parse(src); // LUAU_REQUIRE_ERRORS(res);
LUAU_ASSERT(pr.errors.size() == 0);
LUAU_ASSERT(pr.root->body.size == 1);
AstStat* root = *(pr.root->body.data);
auto func = root->as<AstStatDeclareFunction>();
LUAU_ASSERT(func);
LUAU_ASSERT(func->checkedFunction);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_declared_table_checked_member")
{
std::string src = R"BUILTIN_SRC(
declare math : {
abs : @checked (number) -> number
}
)BUILTIN_SRC";
ParseResult pr = parse(src);
LUAU_ASSERT(pr.errors.size() == 0);
LUAU_ASSERT(pr.root->body.size == 1);
AstStat* root = *(pr.root->body.data);
auto glob = root->as<AstStatDeclareGlobal>();
LUAU_ASSERT(glob);
auto tbl = glob->type->as<AstTypeTable>();
LUAU_ASSERT(tbl);
LUAU_ASSERT(tbl->props.size == 1);
auto prop = *tbl->props.data;
auto func = prop.type->as<AstTypeFunction>();
LUAU_ASSERT(func);
LUAU_ASSERT(func->checkedFunction);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_checked_outside_decl_fails")
{
auto src = R"(
local @checked = 3
)";
ParseResult pr = parse(src);
LUAU_ASSERT(pr.errors.size() > 0);
auto ts = pr.errors[1].getMessage();
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_checked_in_and_out_of_decl_fails")
{
auto src = R"(
local @checked = 3
declare function @checked abs(n: number): number
)";
auto pr = parse(src);
LUAU_ASSERT(pr.errors.size() == 2);
LUAU_ASSERT(pr.errors[0].getLocation().begin.line == 1);
LUAU_ASSERT(pr.errors[1].getLocation().begin.line == 1);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_checked_as_function_name_fails")
{
auto pr = parse(R"(
function @checked(x: number) : number
end
)");
LUAU_ASSERT(pr.errors.size() > 0);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "cannot_use_@_as_variable_name")
{
auto pr = parse(R"(
local @blah = 3
)");
LUAU_ASSERT(pr.errors.size() > 0);
} }
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -3006,4 +3006,110 @@ TEST_CASE_FIXTURE(Fixture, "parse_interpolated_string_with_lookahead_involved2")
REQUIRE_MESSAGE(result.errors.empty(), result.errors[0].getMessage()); REQUIRE_MESSAGE(result.errors.empty(), result.errors[0].getMessage());
} }
TEST_CASE_FIXTURE(Fixture, "parse_top_level_checked_fn")
{
ParseOptions opts;
opts.allowDeclarationSyntax = true;
ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true};
std::string src = R"BUILTIN_SRC(
declare function @checked abs(n: number): number
)BUILTIN_SRC";
ParseResult pr = tryParse(src, opts);
LUAU_ASSERT(pr.errors.size() == 0);
LUAU_ASSERT(pr.root->body.size == 1);
AstStat* root = *(pr.root->body.data);
auto func = root->as<AstStatDeclareFunction>();
LUAU_ASSERT(func);
LUAU_ASSERT(func->checkedFunction);
}
TEST_CASE_FIXTURE(Fixture, "parse_declared_table_checked_member")
{
ParseOptions opts;
opts.allowDeclarationSyntax = true;
ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true};
const std::string src = R"BUILTIN_SRC(
declare math : {
abs : @checked (number) -> number
}
)BUILTIN_SRC";
ParseResult pr = tryParse(src, opts);
LUAU_ASSERT(pr.errors.size() == 0);
LUAU_ASSERT(pr.root->body.size == 1);
AstStat* root = *(pr.root->body.data);
auto glob = root->as<AstStatDeclareGlobal>();
LUAU_ASSERT(glob);
auto tbl = glob->type->as<AstTypeTable>();
LUAU_ASSERT(tbl);
LUAU_ASSERT(tbl->props.size == 1);
auto prop = *tbl->props.data;
auto func = prop.type->as<AstTypeFunction>();
LUAU_ASSERT(func);
LUAU_ASSERT(func->checkedFunction);
}
TEST_CASE_FIXTURE(Fixture, "parse_checked_outside_decl_fails")
{
ParseOptions opts;
opts.allowDeclarationSyntax = true;
ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true};
ParseResult pr = tryParse(R"(
local @checked = 3
)",
opts);
LUAU_ASSERT(pr.errors.size() > 0);
auto ts = pr.errors[1].getMessage();
}
TEST_CASE_FIXTURE(Fixture, "parse_checked_in_and_out_of_decl_fails")
{
ParseOptions opts;
opts.allowDeclarationSyntax = true;
ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true};
auto pr = tryParse(R"(
local @checked = 3
declare function @checked abs(n: number): number
)",
opts);
LUAU_ASSERT(pr.errors.size() == 2);
LUAU_ASSERT(pr.errors[0].getLocation().begin.line == 1);
LUAU_ASSERT(pr.errors[1].getLocation().begin.line == 1);
}
TEST_CASE_FIXTURE(Fixture, "parse_checked_as_function_name_fails")
{
ParseOptions opts;
opts.allowDeclarationSyntax = true;
ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true};
auto pr = tryParse(R"(
function @checked(x: number) : number
end
)",
opts);
LUAU_ASSERT(pr.errors.size() > 0);
}
TEST_CASE_FIXTURE(Fixture, "cannot_use_@_as_variable_name")
{
ParseOptions opts;
opts.allowDeclarationSyntax = true;
ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true};
auto pr = tryParse(R"(
local @blah = 3
)",
opts);
LUAU_ASSERT(pr.errors.size() > 0);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -1038,4 +1038,45 @@ TEST_IS_NOT_SUBTYPE(idx(builtinTypes->stringType, builtinTypes->numberType), tbl
TEST_IS_SUBTYPE(tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}}), tbl({{"X", builtinTypes->numberType}})); TEST_IS_SUBTYPE(tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}}), tbl({{"X", builtinTypes->numberType}}));
TEST_IS_NOT_SUBTYPE(tbl({{"X", builtinTypes->numberType}}), tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}})); TEST_IS_NOT_SUBTYPE(tbl({{"X", builtinTypes->numberType}}), tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}}));
TEST_CASE_FIXTURE(SubtypeFixture, "interior_tests_are_cached")
{
TypeId tableA = tbl({{"X", builtinTypes->numberType}, {"Y", builtinTypes->numberType}});
TypeId tableB = tbl({{"X", builtinTypes->optionalNumberType}, {"Y", builtinTypes->optionalNumberType}});
CHECK_IS_NOT_SUBTYPE(tableA, tableB);
const SubtypingResult* cachedResult = subtyping.peekCache().find({builtinTypes->numberType, builtinTypes->optionalNumberType});
REQUIRE(cachedResult);
CHECK(cachedResult->isSubtype);
cachedResult = subtyping.peekCache().find({tableA, tableB});
REQUIRE(cachedResult);
CHECK(!cachedResult->isSubtype);
}
TEST_CASE_FIXTURE(SubtypeFixture, "results_that_are_contingent_on_generics_are_not_cached")
{
// <T>(T) -> T <: (number) -> number
CHECK_IS_SUBTYPE(genericTToTType, numberToNumberType);
CHECK(subtyping.peekCache().empty());
}
TEST_CASE_FIXTURE(SubtypeFixture, "dont_cache_tests_involving_cycles")
{
TypeId tableA = arena.addType(BlockedType{});
TypeId tableA2 = tbl({{"self", tableA}});
asMutable(tableA)->ty.emplace<BoundType>(tableA2);
TypeId tableB = arena.addType(BlockedType{});
TypeId tableB2 = tbl({{"self", tableB}});
asMutable(tableB)->ty.emplace<BoundType>(tableB2);
CHECK_IS_SUBTYPE(tableA, tableB);
CHECK(!subtyping.peekCache().find({tableA, tableB}));
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -895,13 +895,13 @@ R"(Type
could not be converted into could not be converted into
'{ a: number, b: string, c: { d: number } }' '{ a: number, b: string, c: { d: number } }'
caused by: caused by:
Property 'c' is not compatible. Property 'c' is not compatible.
Type Type
'{| d: string |}' '{| d: string |}'
could not be converted into could not be converted into
'{ d: number }' '{ d: number }'
caused by: caused by:
Property 'd' is not compatible. Property 'd' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)" Type 'string' could not be converted into 'number' in an invariant context)"
: :
R"(Type R"(Type
@ -909,13 +909,13 @@ R"(Type
could not be converted into could not be converted into
'{| a: number, b: string, c: {| d: number |} |}' '{| a: number, b: string, c: {| d: number |} |}'
caused by: caused by:
Property 'c' is not compatible. Property 'c' is not compatible.
Type Type
'{ d: string }' '{ d: string }'
could not be converted into could not be converted into
'{| d: number |}' '{| d: number |}'
caused by: caused by:
Property 'd' is not compatible. Property 'd' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)"; Type 'string' could not be converted into 'number' in an invariant context)";
//clang-format on //clang-format on
// //

View file

@ -149,13 +149,13 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "sort_with_bad_predicate")
could not be converted into could not be converted into
'((string, string) -> boolean)?' '((string, string) -> boolean)?'
caused by: caused by:
None of the union options are compatible. For example: None of the union options are compatible. For example:
Type Type
'(number, number) -> boolean' '(number, number) -> boolean'
could not be converted into could not be converted into
'(string, string) -> boolean' '(string, string) -> boolean'
caused by: caused by:
Argument #1 type is not compatible. Argument #1 type is not compatible.
Type 'string' could not be converted into 'number')"; Type 'string' could not be converted into 'number')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }

View file

@ -383,7 +383,7 @@ b(a)
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'Vector2' could not be converted into '{- X: number, Y: string -}' const std::string expected = R"(Type 'Vector2' could not be converted into '{- X: number, Y: string -}'
caused by: caused by:
Property 'Y' is not compatible. Property 'Y' is not compatible.
Type 'number' could not be converted into 'string')"; Type 'number' could not be converted into 'string')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -466,7 +466,7 @@ local b: B = a
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
const std::string expected = R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property 'x' is not compatible. Property 'x' is not compatible.
Type 'ChildClass' could not be converted into 'BaseClass' in an invariant context)"; Type 'ChildClass' could not be converted into 'BaseClass' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }

View file

@ -1373,7 +1373,7 @@ local b: B = a
could not be converted into could not be converted into
'(number, string) -> string' '(number, string) -> string'
caused by: caused by:
Argument #2 type is not compatible. Argument #2 type is not compatible.
Type 'string' could not be converted into 'number')"; Type 'string' could not be converted into 'number')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -1414,7 +1414,7 @@ local b: B = a
could not be converted into could not be converted into
'(number, number) -> number' '(number, number) -> number'
caused by: caused by:
Return type is not compatible. Return type is not compatible.
Type 'string' could not be converted into 'number')"; Type 'string' could not be converted into 'number')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -1435,7 +1435,7 @@ local b: B = a
could not be converted into could not be converted into
'(number, number) -> (number, boolean)' '(number, number) -> (number, boolean)'
caused by: caused by:
Return #2 type is not compatible. Return #2 type is not compatible.
Type 'string' could not be converted into 'boolean')"; Type 'string' could not be converted into 'boolean')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -1565,13 +1565,13 @@ end
could not be converted into could not be converted into
'((number) -> number)?' '((number) -> number)?'
caused by: caused by:
None of the union options are compatible. For example: None of the union options are compatible. For example:
Type Type
'(string) -> string' '(string) -> string'
could not be converted into could not be converted into
'(number) -> number' '(number) -> number'
caused by: caused by:
Argument #1 type is not compatible. Argument #1 type is not compatible.
Type 'number' could not be converted into 'string')"); Type 'number' could not be converted into 'string')");
CHECK_EQ(toString(result.errors[1]), R"(Type 'string' could not be converted into 'number')"); CHECK_EQ(toString(result.errors[1]), R"(Type 'string' could not be converted into 'number')");
} }
@ -2042,7 +2042,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "param_1_and_2_both_takes_the_same_generic_bu
const std::string expected = R"(Type '{ x: number }' could not be converted into 'vec2?' const std::string expected = R"(Type '{ x: number }' could not be converted into 'vec2?'
caused by: caused by:
None of the union options are compatible. For example: None of the union options are compatible. For example:
Table type '{ x: number }' not compatible with type 'vec2' because the former is missing field 'y')"; Table type '{ x: number }' not compatible with type 'vec2' because the former is missing field 'y')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
CHECK_EQ("Type 'vec2' could not be converted into 'number'", toString(result.errors[1])); CHECK_EQ("Type 'vec2' could not be converted into 'number'", toString(result.errors[1]));

View file

@ -727,10 +727,10 @@ y.a.c = y
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
const std::string expected = R"(Type 'y' could not be converted into 'T<string>' const std::string expected = R"(Type 'y' could not be converted into 'T<string>'
caused by: caused by:
Property 'a' is not compatible. Property 'a' is not compatible.
Type '{ c: T<string>?, d: number }' could not be converted into 'U<string>' Type '{ c: T<string>?, d: number }' could not be converted into 'U<string>'
caused by: caused by:
Property 'd' is not compatible. Property 'd' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)"; Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }

View file

@ -397,7 +397,7 @@ local a: XYZ = 3
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'number' could not be converted into 'X & Y & Z' const std::string expected = R"(Type 'number' could not be converted into 'X & Y & Z'
caused by: caused by:
Not all intersection parts are compatible. Not all intersection parts are compatible.
Type 'number' could not be converted into 'X')"; Type 'number' could not be converted into 'X')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }

View file

@ -412,7 +412,7 @@ local b: B.T = a
CheckResult result = frontend.check("game/C"); CheckResult result = frontend.check("game/C");
const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B' const std::string expected = R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'
caused by: caused by:
Property 'x' is not compatible. Property 'x' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)"; Type 'number' could not be converted into 'string' in an invariant context)";
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
@ -447,7 +447,7 @@ local b: B.T = a
CheckResult result = frontend.check("game/D"); CheckResult result = frontend.check("game/D");
const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C' const std::string expected = R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'
caused by: caused by:
Property 'x' is not compatible. Property 'x' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)"; Type 'number' could not be converted into 'string' in an invariant context)";
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));

View file

@ -263,13 +263,18 @@ TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_have_a_m
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
GenericError* gen = get<GenericError>(result.errors[0]);
REQUIRE(gen != nullptr);
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(gen->message == "Types 'a' and 'b' cannot be compared with < because neither type has a metatable"); {
UninhabitedTypeFamily* utf = get<UninhabitedTypeFamily>(result.errors[0]);
REQUIRE(utf);
REQUIRE_EQ(toString(utf->ty), "lt<a, b>");
}
else else
{
GenericError* gen = get<GenericError>(result.errors[0]);
REQUIRE(gen != nullptr);
REQUIRE_EQ(gen->message, "Type a cannot be compared with < because it has no metatable"); REQUIRE_EQ(gen->message, "Type a cannot be compared with < because it has no metatable");
}
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "cannot_indirectly_compare_types_that_do_not_offer_overloaded_ordering_operators") TEST_CASE_FIXTURE(BuiltinsFixture, "cannot_indirectly_compare_types_that_do_not_offer_overloaded_ordering_operators")
@ -288,13 +293,18 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cannot_indirectly_compare_types_that_do_not_
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
GenericError* gen = get<GenericError>(result.errors[0]);
REQUIRE(gen != nullptr);
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(gen->message == "Types 'M' and 'M' cannot be compared with < because neither type's metatable has a '__lt' metamethod"); {
UninhabitedTypeFamily* utf = get<UninhabitedTypeFamily>(result.errors[0]);
REQUIRE(utf);
REQUIRE_EQ(toString(utf->ty), "lt<M, M>");
}
else else
{
GenericError* gen = get<GenericError>(result.errors[0]);
REQUIRE(gen != nullptr);
REQUIRE_EQ(gen->message, "Table M does not offer metamethod __lt"); REQUIRE_EQ(gen->message, "Table M does not offer metamethod __lt");
}
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "cannot_compare_tables_that_do_not_have_the_same_metatable") TEST_CASE_FIXTURE(BuiltinsFixture, "cannot_compare_tables_that_do_not_have_the_same_metatable")
@ -727,13 +737,18 @@ TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operato
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
GenericError* ge = get<GenericError>(result.errors[0]);
REQUIRE(ge);
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("Types 'boolean' and 'boolean' cannot be compared with relational operator <", ge->message); {
UninhabitedTypeFamily* utf = get<UninhabitedTypeFamily>(result.errors[0]);
REQUIRE(utf);
REQUIRE_EQ(toString(utf->ty), "lt<boolean, boolean>");
}
else else
{
GenericError* ge = get<GenericError>(result.errors[0]);
REQUIRE(ge);
CHECK_EQ("Type 'boolean' cannot be compared with relational operator <", ge->message); CHECK_EQ("Type 'boolean' cannot be compared with relational operator <", ge->message);
}
} }
TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators2") TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators2")
@ -746,12 +761,18 @@ TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operato
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
GenericError* ge = get<GenericError>(result.errors[0]);
REQUIRE(ge);
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("Types 'number | string' and 'number | string' cannot be compared with relational operator <", ge->message); {
UninhabitedTypeFamily* utf = get<UninhabitedTypeFamily>(result.errors[0]);
REQUIRE(utf);
REQUIRE_EQ(toString(utf->ty), "lt<number | string, number | string>");
}
else else
{
GenericError* ge = get<GenericError>(result.errors[0]);
REQUIRE(ge);
CHECK_EQ("Type 'number | string' cannot be compared with relational operator <", ge->message); CHECK_EQ("Type 'number | string' cannot be compared with relational operator <", ge->message);
}
} }
TEST_CASE_FIXTURE(Fixture, "cli_38355_recursive_union") TEST_CASE_FIXTURE(Fixture, "cli_38355_recursive_union")
@ -915,7 +936,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_any_in_all_modes_when_lhs_is_unknown")
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> Add<a, b>"); CHECK(toString(requireType("f")) == "<a, b>(a, b) -> add<a, b>");
} }
else else
{ {
@ -947,7 +968,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_subtraction")
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> Sub<a, b>"); CHECK(toString(requireType("f")) == "<a, b>(a, b) -> sub<a, b>");
} }
else else
{ {
@ -967,7 +988,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_multiplication")
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> Mul<a, b>"); CHECK(toString(requireType("f")) == "<a, b>(a, b) -> mul<a, b>");
} }
else else
{ {
@ -987,7 +1008,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_division")
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> Div<a, b>"); CHECK(toString(requireType("f")) == "<a, b>(a, b) -> div<a, b>");
} }
else else
{ {
@ -1009,7 +1030,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_floor_division")
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> FloorDiv<a, b>"); CHECK(toString(requireType("f")) == "<a, b>(a, b) -> idiv<a, b>");
} }
else else
{ {
@ -1029,7 +1050,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_exponentiation")
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> Exp<a, b>"); CHECK(toString(requireType("f")) == "<a, b>(a, b) -> pow<a, b>");
} }
else else
{ {
@ -1049,7 +1070,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_modulo")
if (FFlag::DebugLuauDeferredConstraintResolution) if (FFlag::DebugLuauDeferredConstraintResolution)
{ {
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> Mod<a, b>"); CHECK(toString(requireType("f")) == "<a, b>(a, b) -> mod<a, b>");
} }
else else
{ {
@ -1058,6 +1079,26 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_modulo")
} }
} }
TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_concat")
{
CheckResult result = check(Mode::Strict, R"(
local function f(x, y)
return x .. y
end
)");
if (FFlag::DebugLuauDeferredConstraintResolution)
{
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("f")) == "<a, b>(a, b) -> concat<a, b>");
}
else
{
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Unknown type used in .. operation; consider adding a type annotation to 'x'");
}
}
TEST_CASE_FIXTURE(BuiltinsFixture, "equality_operations_succeed_if_any_union_branch_succeeds") TEST_CASE_FIXTURE(BuiltinsFixture, "equality_operations_succeed_if_any_union_branch_succeeds")
{ {
CheckResult result = check(R"( CheckResult result = check(R"(

View file

@ -817,7 +817,7 @@ TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_ty
could not be converted into could not be converted into
'{| x: number |}' '{| x: number |}'
caused by: caused by:
Property 'x' is not compatible. Property 'x' is not compatible.
Type 'number?' could not be converted into 'number' in an invariant context)"; Type 'number?' could not be converted into 'number' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -1099,4 +1099,40 @@ foo(1 :: any)
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "luau_roact_useState_minimization")
{
// We don't expect this test to work on the old solver, but it also does not yet work on the new solver.
// So, we can't just put a scoped fast flag here, or it would block CI.
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
type BasicStateAction<S> = ((S) -> S) | S
type Dispatch<A> = (A) -> ()
local function useState<S>(
initialState: (() -> S) | S
): (S, Dispatch<BasicStateAction<S>>)
-- fake impl that obeys types
local val = if type(initialState) == "function" then initialState() else initialState
return val, function(value)
return value
end
end
local test, setTest = useState(nil :: string?)
setTest(nil) -- this line causes the type to be narrowed in the old solver!!!
local function update(value: string)
print(test)
setTest(value)
end
update("hello")
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END(); TEST_SUITE_END();

View file

@ -1908,22 +1908,23 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "refine_unknown_to_table")
// this test is DCR-only as an instance of DCR fixing a bug in the old solver // this test is DCR-only as an instance of DCR fixing a bug in the old solver
CheckResult result = check(R"( CheckResult result = check(R"(
local a : unknown = nil
local idx, val local idx, val
if typeof(a) == "table" then local function f(a: unknown)
for i, v in a do if typeof(a) == "table" then
idx = i for i, v in a do
val = v idx = i
val = v
end
end end
end end
)"); )");
LUAU_REQUIRE_NO_ERRORS(result); LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("unknown", toString(requireType("idx"))); // TODO: they should be `unknown`, not `nil`.
CHECK_EQ("unknown", toString(requireType("val"))); CHECK_EQ("nil", toString(requireType("idx")));
CHECK_EQ("nil", toString(requireType("val")));
} }
TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing") TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing")

View file

@ -327,7 +327,7 @@ local a: Animal = { tag = 'cat', cafood = 'something' }
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'a' could not be converted into 'Cat | Dog' const std::string expected = R"(Type 'a' could not be converted into 'Cat | Dog'
caused by: caused by:
None of the union options are compatible. For example: None of the union options are compatible. For example:
Table type 'a' not compatible with type 'Cat' because the former is missing field 'catfood')"; Table type 'a' not compatible with type 'Cat' because the former is missing field 'catfood')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -345,7 +345,7 @@ local a: Result = { success = false, result = 'something' }
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'a' could not be converted into 'Bad | Good' const std::string expected = R"(Type 'a' could not be converted into 'Bad | Good'
caused by: caused by:
None of the union options are compatible. For example: None of the union options are compatible. For example:
Table type 'a' not compatible with type 'Bad' because the former is missing field 'error')"; Table type 'a' not compatible with type 'Bad' because the former is missing field 'error')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }

View file

@ -2119,7 +2119,7 @@ local b: B = a
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
const std::string expected = R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property 'y' is not compatible. Property 'y' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)"; Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -2140,10 +2140,10 @@ local b: B = a
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
const std::string expected = R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property 'b' is not compatible. Property 'b' is not compatible.
Type 'AS' could not be converted into 'BS' Type 'AS' could not be converted into 'BS'
caused by: caused by:
Property 'y' is not compatible. Property 'y' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)"; Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -2167,7 +2167,7 @@ caused by:
could not be converted into could not be converted into
'{ x: number, y: number }' '{ x: number, y: number }'
caused by: caused by:
Property 'y' is not compatible. Property 'y' is not compatible.
Type 'string' could not be converted into 'number' in an invariant context)"; Type 'string' could not be converted into 'number' in an invariant context)";
const std::string expected2 = R"(Type 'b2' could not be converted into 'a2' const std::string expected2 = R"(Type 'b2' could not be converted into 'a2'
caused by: caused by:
@ -2176,7 +2176,7 @@ caused by:
could not be converted into could not be converted into
'{ __call: <a>(a) -> () }' '{ __call: <a>(a) -> () }'
caused by: caused by:
Property '__call' is not compatible. Property '__call' is not compatible.
Type Type
'<a, b>(a, b) -> ()' '<a, b>(a, b) -> ()'
could not be converted into could not be converted into
@ -2188,7 +2188,7 @@ caused by:
could not be converted into could not be converted into
'{ __call: <a>(a) -> () }' '{ __call: <a>(a) -> () }'
caused by: caused by:
Property '__call' is not compatible. Property '__call' is not compatible.
Type Type
'<a, b>(a, b) -> ()' '<a, b>(a, b) -> ()'
could not be converted into could not be converted into
@ -2209,7 +2209,7 @@ caused by:
could not be converted into could not be converted into
'{ __call: <a>(a) -> () }' '{ __call: <a>(a) -> () }'
caused by: caused by:
Property '__call' is not compatible. Property '__call' is not compatible.
Type Type
'(a, b) -> ()' '(a, b) -> ()'
could not be converted into could not be converted into
@ -2231,7 +2231,7 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key")
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
const std::string expected = R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property '[indexer key]' is not compatible. Property '[indexer key]' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)"; Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -2249,7 +2249,7 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_value")
LUAU_REQUIRE_ERRORS(result); LUAU_REQUIRE_ERRORS(result);
const std::string expected = R"(Type 'A' could not be converted into 'B' const std::string expected = R"(Type 'A' could not be converted into 'B'
caused by: caused by:
Property '[indexer value]' is not compatible. Property '[indexer value]' is not compatible.
Type 'number' could not be converted into 'string' in an invariant context)"; Type 'number' could not be converted into 'string' in an invariant context)";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -2287,7 +2287,7 @@ local y: number = tmp.p.y
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'tmp' could not be converted into 'HasSuper' const std::string expected = R"(Type 'tmp' could not be converted into 'HasSuper'
caused by: caused by:
Property 'p' is not compatible. Property 'p' is not compatible.
Table type '{ x: number, y: number }' not compatible with type 'Super' because the former has extra field 'y')"; Table type '{ x: number, y: number }' not compatible with type 'Super' because the former has extra field 'y')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -3399,7 +3399,7 @@ TEST_CASE_FIXTURE(Fixture, "scalar_is_not_a_subtype_of_a_compatible_polymorphic_
const std::string expected1 = const std::string expected1 =
R"(Type 'string' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' R"(Type 'string' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by: caused by:
The former's metatable does not satisfy the requirements. The former's metatable does not satisfy the requirements.
Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')";
CHECK_EQ(expected1, toString(result.errors[0])); CHECK_EQ(expected1, toString(result.errors[0]));
@ -3407,7 +3407,7 @@ Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutel
const std::string expected2 = const std::string expected2 =
R"(Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' R"(Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by: caused by:
The former's metatable does not satisfy the requirements. The former's metatable does not satisfy the requirements.
Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')";
CHECK_EQ(expected2, toString(result.errors[1])); CHECK_EQ(expected2, toString(result.errors[1]));
@ -3416,10 +3416,10 @@ Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutel
could not be converted into could not be converted into
't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by: caused by:
Not all union options are compatible. Not all union options are compatible.
Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' Type '"bar"' could not be converted into 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}'
caused by: caused by:
The former's metatable does not satisfy the requirements. The former's metatable does not satisfy the requirements.
Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; Table type 'typeof(string)' not compatible with type 't1 where t1 = {- absolutely_no_scalar_has_this_method: (t1) -> (a...) -}' because the former is missing field 'absolutely_no_scalar_has_this_method')";
CHECK_EQ(expected3, toString(result.errors[2])); CHECK_EQ(expected3, toString(result.errors[2]));
} }
@ -3450,7 +3450,7 @@ TEST_CASE_FIXTURE(Fixture, "a_free_shape_cannot_turn_into_a_scalar_if_it_is_not_
const std::string expected = const std::string expected =
R"(Type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' could not be converted into 'string' R"(Type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' could not be converted into 'string'
caused by: caused by:
The former's metatable does not satisfy the requirements. The former's metatable does not satisfy the requirements.
Table type 'typeof(string)' not compatible with type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' because the former is missing field 'absolutely_no_scalar_has_this_method')"; Table type 'typeof(string)' not compatible with type 't1 where t1 = {+ absolutely_no_scalar_has_this_method: (t1) -> (a, b...) +}' because the former is missing field 'absolutely_no_scalar_has_this_method')";
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));

View file

@ -1045,19 +1045,19 @@ TEST_CASE_FIXTURE(Fixture, "cli_50041_committing_txnlog_in_apollo_client_error")
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'Policies' from 'MainModule' could not be converted into 'Policies' from 'MainModule' const std::string expected = R"(Type 'Policies' from 'MainModule' could not be converted into 'Policies' from 'MainModule'
caused by: caused by:
Property 'getStoreFieldName' is not compatible. Property 'getStoreFieldName' is not compatible.
Type Type
'(Policies, FieldSpecifier & {| from: number? |}) -> (a, b...)' '(Policies, FieldSpecifier & {| from: number? |}) -> (a, b...)'
could not be converted into could not be converted into
'(Policies, FieldSpecifier) -> string' '(Policies, FieldSpecifier) -> string'
caused by: caused by:
Argument #2 type is not compatible. Argument #2 type is not compatible.
Type Type
'FieldSpecifier' 'FieldSpecifier'
could not be converted into could not be converted into
'FieldSpecifier & {| from: number? |}' 'FieldSpecifier & {| from: number? |}'
caused by: caused by:
Not all intersection parts are compatible. Not all intersection parts are compatible.
Table type 'FieldSpecifier' not compatible with type '{| from: number? |}' because the former has extra field 'fieldName')"; Table type 'FieldSpecifier' not compatible with type '{| from: number? |}' because the former has extra field 'fieldName')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }

View file

@ -0,0 +1,122 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Fixture.h"
#include "doctest.h"
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
using namespace Luau;
namespace
{
struct TypeStateFixture : BuiltinsFixture
{
ScopedFastFlag dcr{"DebugLuauDeferredConstraintResolution", true};
};
}
TEST_SUITE_BEGIN("TypeStatesTest");
TEST_CASE_FIXTURE(TypeStateFixture, "initialize_x_of_type_string_or_nil_with_nil")
{
CheckResult result = check(R"(
local x: string? = nil
local a = x
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("nil" == toString(requireType("a")));
}
TEST_CASE_FIXTURE(TypeStateFixture, "extraneous_lvalues_are_populated_with_nil")
{
CheckResult result = check(R"(
local function f(): (string, number)
return "hello", 5
end
local x, y, z = f()
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK("Function only returns 2 values, but 3 are required here" == toString(result.errors[0]));
CHECK("string" == toString(requireType("x")));
CHECK("number" == toString(requireType("y")));
CHECK("nil" == toString(requireType("z")));
}
TEST_CASE_FIXTURE(TypeStateFixture, "assign_different_values_to_x")
{
CheckResult result = check(R"(
local x: string? = nil
local a = x
x = "hello!"
local b = x
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("nil" == toString(requireType("a")));
CHECK("string" == toString(requireType("b")));
}
TEST_CASE_FIXTURE(TypeStateFixture, "parameter_x_was_constrained_by_two_types")
{
// Parameter `x` has a fresh type `'x` bounded by `never` and `unknown`.
// The first use of `x` constrains `x`'s upper bound by `string | number`.
// The second use of `x`, aliased by `y`, constrains `x`'s upper bound by `string?`.
// This results in `'x <: (string | number) & (string?)`.
// The principal type of the upper bound is `string`.
CheckResult result = check(R"(
local function f(x): string?
local y: string | number = x
return y
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("(string) -> string?" == toString(requireType("f")));
}
#if 0
TEST_CASE_FIXTURE(TypeStateFixture, "local_that_will_be_assigned_later")
{
CheckResult result = check(R"(
local x: string
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(TypeStateFixture, "refine_a_local_and_then_assign_it")
{
CheckResult result = check(R"(
local function f(x: string?)
if typeof(x) == "string" then
x = nil
end
local y: nil = x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
#endif
TEST_CASE_FIXTURE(TypeStateFixture, "assign_a_local_and_then_refine_it")
{
CheckResult result = check(R"(
local function f(x: string?)
x = nil
if typeof(x) == "string" then
local y: typeof(x) = "hello"
end
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK("Type 'string' could not be converted into 'never'" == toString(result.errors[0]));
}
TEST_SUITE_END();

View file

@ -222,9 +222,9 @@ TEST_CASE_FIXTURE(Fixture, "union_equality_comparisons")
type B = number | nil type B = number | nil
type C = number | boolean type C = number | boolean
local a: A = 1 local a = 1 :: A
local b: B = nil local b = nil :: B
local c: C = true local c = true :: C
local n = 1 local n = 1
local x = a == b local x = a == b
@ -517,7 +517,7 @@ local a: X? = { w = 4 }
LUAU_REQUIRE_ERROR_COUNT(1, result); LUAU_REQUIRE_ERROR_COUNT(1, result);
const std::string expected = R"(Type 'a' could not be converted into 'X?' const std::string expected = R"(Type 'a' could not be converted into 'X?'
caused by: caused by:
None of the union options are compatible. For example: None of the union options are compatible. For example:
Table type 'a' not compatible with type 'X' because the former is missing field 'x')"; Table type 'a' not compatible with type 'X' because the former is missing field 'x')";
CHECK_EQ(expected, toString(result.errors[0])); CHECK_EQ(expected, toString(result.errors[0]));
} }
@ -843,7 +843,7 @@ TEST_CASE_FIXTURE(Fixture, "suppress_errors_for_prop_lookup_of_a_union_that_incl
registerHiddenTypes(&frontend); registerHiddenTypes(&frontend);
CheckResult result = check(R"( CheckResult result = check(R"(
local a : err | Not<nil> local a = 5 :: err | Not<nil>
local b = a.foo local b = a.foo
)"); )");

View file

@ -1,16 +1,24 @@
AnnotationTests.cloned_interface_maintains_pointers_between_definitions
AnnotationTests.infer_type_of_value_a_via_typeof_with_assignment AnnotationTests.infer_type_of_value_a_via_typeof_with_assignment
AnnotationTests.table_annotation
AnnotationTests.two_type_params AnnotationTests.two_type_params
AnnotationTests.typeof_expr
AnnotationTests.use_generic_type_alias AnnotationTests.use_generic_type_alias
AstQuery.last_argument_function_call_type AstQuery.last_argument_function_call_type
AstQuery::getDocumentationSymbolAtPosition.class_method
AstQuery::getDocumentationSymbolAtPosition.overloaded_class_method
AstQuery::getDocumentationSymbolAtPosition.table_overloaded_function_prop AstQuery::getDocumentationSymbolAtPosition.table_overloaded_function_prop
AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg
AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg
AutocompleteTest.autocomplete_interpolated_string_as_singleton AutocompleteTest.autocomplete_interpolated_string_as_singleton
AutocompleteTest.autocomplete_oop_implicit_self AutocompleteTest.autocomplete_oop_implicit_self
AutocompleteTest.autocomplete_string_singleton_equality
AutocompleteTest.autocomplete_string_singleton_escape AutocompleteTest.autocomplete_string_singleton_escape
AutocompleteTest.autocomplete_string_singletons AutocompleteTest.autocomplete_string_singletons
AutocompleteTest.cyclic_table
AutocompleteTest.do_wrong_compatible_nonself_calls AutocompleteTest.do_wrong_compatible_nonself_calls
AutocompleteTest.frontend_use_correct_global_scope
AutocompleteTest.no_incompatible_self_calls_on_class
AutocompleteTest.string_singleton_in_if_statement
AutocompleteTest.suggest_external_module_type AutocompleteTest.suggest_external_module_type
AutocompleteTest.type_correct_expected_argument_type_pack_suggestion AutocompleteTest.type_correct_expected_argument_type_pack_suggestion
AutocompleteTest.type_correct_expected_argument_type_suggestion AutocompleteTest.type_correct_expected_argument_type_suggestion
@ -23,7 +31,6 @@ AutocompleteTest.type_correct_function_return_types
AutocompleteTest.type_correct_keywords AutocompleteTest.type_correct_keywords
AutocompleteTest.type_correct_suggestion_for_overloads AutocompleteTest.type_correct_suggestion_for_overloads
AutocompleteTest.type_correct_suggestion_in_argument AutocompleteTest.type_correct_suggestion_in_argument
AutocompleteTest.unsealed_table_2
BuiltinTests.aliased_string_format BuiltinTests.aliased_string_format
BuiltinTests.assert_removes_falsy_types BuiltinTests.assert_removes_falsy_types
BuiltinTests.assert_removes_falsy_types2 BuiltinTests.assert_removes_falsy_types2
@ -32,6 +39,7 @@ BuiltinTests.assert_returns_false_and_string_iff_it_knows_the_first_argument_can
BuiltinTests.bad_select_should_not_crash BuiltinTests.bad_select_should_not_crash
BuiltinTests.coroutine_resume_anything_goes BuiltinTests.coroutine_resume_anything_goes
BuiltinTests.debug_info_is_crazy BuiltinTests.debug_info_is_crazy
BuiltinTests.dont_add_definitions_to_persistent_types
BuiltinTests.global_singleton_types_are_sealed BuiltinTests.global_singleton_types_are_sealed
BuiltinTests.gmatch_capture_types BuiltinTests.gmatch_capture_types
BuiltinTests.gmatch_capture_types2 BuiltinTests.gmatch_capture_types2
@ -59,6 +67,7 @@ BuiltinTests.string_format_as_method
BuiltinTests.string_format_correctly_ordered_types BuiltinTests.string_format_correctly_ordered_types
BuiltinTests.string_format_report_all_type_errors_at_correct_positions BuiltinTests.string_format_report_all_type_errors_at_correct_positions
BuiltinTests.string_format_use_correct_argument2 BuiltinTests.string_format_use_correct_argument2
BuiltinTests.string_match
BuiltinTests.table_concat_returns_string BuiltinTests.table_concat_returns_string
BuiltinTests.table_dot_remove_optionally_returns_generic BuiltinTests.table_dot_remove_optionally_returns_generic
BuiltinTests.table_freeze_is_generic BuiltinTests.table_freeze_is_generic
@ -93,10 +102,12 @@ ControlFlowAnalysis.tagged_unions_breaking
ControlFlowAnalysis.tagged_unions_continuing ControlFlowAnalysis.tagged_unions_continuing
ControlFlowAnalysis.type_alias_does_not_leak_out_breaking ControlFlowAnalysis.type_alias_does_not_leak_out_breaking
ControlFlowAnalysis.type_alias_does_not_leak_out_continuing ControlFlowAnalysis.type_alias_does_not_leak_out_continuing
DefinitionTests.class_definition_function_prop
DefinitionTests.class_definition_indexer DefinitionTests.class_definition_indexer
DefinitionTests.class_definition_overload_metamethods DefinitionTests.class_definition_overload_metamethods
DefinitionTests.class_definition_string_props DefinitionTests.class_definition_string_props
DefinitionTests.declaring_generic_functions DefinitionTests.declaring_generic_functions
DefinitionTests.definition_file_class_function_args
DefinitionTests.definition_file_classes DefinitionTests.definition_file_classes
Differ.equal_generictp_cyclic Differ.equal_generictp_cyclic
Differ.equal_table_A_B_C Differ.equal_table_A_B_C
@ -119,9 +130,10 @@ Differ.table_left_circle_right_measuring_tape
FrontendTest.accumulate_cached_errors_in_consistent_order FrontendTest.accumulate_cached_errors_in_consistent_order
FrontendTest.it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded FrontendTest.it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded
GenericsTests.apply_type_function_nested_generics1 GenericsTests.apply_type_function_nested_generics1
GenericsTests.apply_type_function_nested_generics3 GenericsTests.apply_type_function_nested_generics2
GenericsTests.better_mismatch_error_messages GenericsTests.better_mismatch_error_messages
GenericsTests.bound_tables_do_not_clone_original_fields GenericsTests.bound_tables_do_not_clone_original_fields
GenericsTests.calling_self_generic_methods
GenericsTests.check_generic_function GenericsTests.check_generic_function
GenericsTests.check_generic_local_function GenericsTests.check_generic_local_function
GenericsTests.check_generic_typepack_function GenericsTests.check_generic_typepack_function
@ -169,11 +181,17 @@ GenericsTests.rank_N_types_via_typeof
GenericsTests.self_recursive_instantiated_param GenericsTests.self_recursive_instantiated_param
GenericsTests.type_parameters_can_be_polytypes GenericsTests.type_parameters_can_be_polytypes
GenericsTests.typefuns_sharing_types GenericsTests.typefuns_sharing_types
IntersectionTypes.argument_is_intersection
IntersectionTypes.error_detailed_intersection_all IntersectionTypes.error_detailed_intersection_all
IntersectionTypes.error_detailed_intersection_part IntersectionTypes.error_detailed_intersection_part
IntersectionTypes.fx_intersection_as_argument
IntersectionTypes.index_on_an_intersection_type_with_mixed_types
IntersectionTypes.index_on_an_intersection_type_with_one_part_missing_the_property
IntersectionTypes.index_on_an_intersection_type_with_one_property_of_type_any
IntersectionTypes.index_on_an_intersection_type_with_property_guaranteed_to_exist
IntersectionTypes.index_on_an_intersection_type_works_at_arbitrary_depth
IntersectionTypes.intersect_bool_and_false IntersectionTypes.intersect_bool_and_false
IntersectionTypes.intersect_false_and_bool_and_false IntersectionTypes.intersect_false_and_bool_and_false
IntersectionTypes.intersect_metatables
IntersectionTypes.intersect_saturate_overloaded_functions IntersectionTypes.intersect_saturate_overloaded_functions
IntersectionTypes.intersection_of_tables IntersectionTypes.intersection_of_tables
IntersectionTypes.intersection_of_tables_with_never_properties IntersectionTypes.intersection_of_tables_with_never_properties
@ -192,12 +210,20 @@ IntersectionTypes.overloadeded_functions_with_weird_typepacks_1
IntersectionTypes.overloadeded_functions_with_weird_typepacks_2 IntersectionTypes.overloadeded_functions_with_weird_typepacks_2
IntersectionTypes.overloadeded_functions_with_weird_typepacks_3 IntersectionTypes.overloadeded_functions_with_weird_typepacks_3
IntersectionTypes.overloadeded_functions_with_weird_typepacks_4 IntersectionTypes.overloadeded_functions_with_weird_typepacks_4
IntersectionTypes.propagates_name
IntersectionTypes.select_correct_union_fn IntersectionTypes.select_correct_union_fn
IntersectionTypes.should_still_pick_an_overload_whose_arguments_are_unions IntersectionTypes.should_still_pick_an_overload_whose_arguments_are_unions
IntersectionTypes.table_extra_ok
IntersectionTypes.table_intersection_setmetatable
IntersectionTypes.table_intersection_write_sealed
IntersectionTypes.table_intersection_write_sealed_indirect IntersectionTypes.table_intersection_write_sealed_indirect
IntersectionTypes.table_write_sealed_indirect IntersectionTypes.table_write_sealed_indirect
IntersectionTypes.union_saturate_overloaded_functions IntersectionTypes.union_saturate_overloaded_functions
Linter.DeprecatedApiFenv
Linter.FormatStringTyped
Linter.TableOperationsIndexer
Negations.cofinite_strings_can_be_compared_for_equality Negations.cofinite_strings_can_be_compared_for_equality
Negations.negated_string_is_a_subtype_of_string
Normalize.higher_order_function_with_annotation Normalize.higher_order_function_with_annotation
Normalize.negations_of_tables Normalize.negations_of_tables
Normalize.specific_functions_cannot_be_negated Normalize.specific_functions_cannot_be_negated
@ -212,10 +238,12 @@ ProvisionalTests.free_options_can_be_unified_together
ProvisionalTests.free_options_cannot_be_unified_together ProvisionalTests.free_options_cannot_be_unified_together
ProvisionalTests.function_returns_many_things_but_first_of_it_is_forgotten ProvisionalTests.function_returns_many_things_but_first_of_it_is_forgotten
ProvisionalTests.generic_type_leak_to_module_interface ProvisionalTests.generic_type_leak_to_module_interface
ProvisionalTests.generic_type_leak_to_module_interface_variadic
ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns
ProvisionalTests.it_should_be_agnostic_of_actual_size ProvisionalTests.it_should_be_agnostic_of_actual_size
ProvisionalTests.luau-polyfill.Array.filter ProvisionalTests.luau-polyfill.Array.filter
ProvisionalTests.luau-polyfill.Map.entries ProvisionalTests.luau-polyfill.Map.entries
ProvisionalTests.luau_roact_useState_minimization
ProvisionalTests.optional_class_instances_are_invariant ProvisionalTests.optional_class_instances_are_invariant
ProvisionalTests.pcall_returns_at_least_two_value_but_function_returns_nothing ProvisionalTests.pcall_returns_at_least_two_value_but_function_returns_nothing
ProvisionalTests.setmetatable_constrains_free_type_into_free_table ProvisionalTests.setmetatable_constrains_free_type_into_free_table
@ -223,9 +251,11 @@ ProvisionalTests.specialization_binds_with_prototypes_too_early
ProvisionalTests.table_insert_with_a_singleton_argument ProvisionalTests.table_insert_with_a_singleton_argument
ProvisionalTests.table_unification_infinite_recursion ProvisionalTests.table_unification_infinite_recursion
ProvisionalTests.typeguard_inference_incomplete ProvisionalTests.typeguard_inference_incomplete
ProvisionalTests.weirditer_should_not_loop_forever
ProvisionalTests.while_body_are_also_refined ProvisionalTests.while_body_are_also_refined
ProvisionalTests.xpcall_returns_what_f_returns ProvisionalTests.xpcall_returns_what_f_returns
RefinementTest.apply_refinements_on_astexprindexexpr_whose_subscript_expr_is_constant_string RefinementTest.apply_refinements_on_astexprindexexpr_whose_subscript_expr_is_constant_string
RefinementTest.assert_a_to_be_truthy_then_assert_a_to_be_number
RefinementTest.correctly_lookup_property_whose_base_was_previously_refined RefinementTest.correctly_lookup_property_whose_base_was_previously_refined
RefinementTest.dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never RefinementTest.dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never
RefinementTest.discriminate_from_truthiness_of_x RefinementTest.discriminate_from_truthiness_of_x
@ -241,6 +271,7 @@ RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true
RefinementTest.not_t_or_some_prop_of_t RefinementTest.not_t_or_some_prop_of_t
RefinementTest.refine_a_property_of_some_global RefinementTest.refine_a_property_of_some_global
RefinementTest.refine_unknown_to_table_then_clone_it RefinementTest.refine_unknown_to_table_then_clone_it
RefinementTest.refinements_should_preserve_error_suppression
RefinementTest.string_not_equal_to_string_or_nil RefinementTest.string_not_equal_to_string_or_nil
RefinementTest.truthy_constraint_on_properties RefinementTest.truthy_constraint_on_properties
RefinementTest.type_annotations_arent_relevant_when_doing_dataflow_analysis RefinementTest.type_annotations_arent_relevant_when_doing_dataflow_analysis
@ -294,22 +325,27 @@ TableTests.explicitly_typed_table_with_indexer
TableTests.extend_unsealed_table_with_metatable TableTests.extend_unsealed_table_with_metatable
TableTests.generalize_table_argument TableTests.generalize_table_argument
TableTests.generic_table_instantiation_potential_regression TableTests.generic_table_instantiation_potential_regression
TableTests.give_up_after_one_metatable_index_look_up
TableTests.indexer_mismatch TableTests.indexer_mismatch
TableTests.indexer_on_sealed_table_must_unify_with_free_table TableTests.indexer_on_sealed_table_must_unify_with_free_table
TableTests.indexers_get_quantified_too TableTests.indexers_get_quantified_too
TableTests.indexing_from_a_table_should_prefer_properties_when_possible
TableTests.inequality_operators_imply_exactly_matching_types TableTests.inequality_operators_imply_exactly_matching_types
TableTests.infer_array_2 TableTests.infer_array_2
TableTests.infer_indexer_for_left_unsealed_table_from_right_hand_table_with_indexer TableTests.infer_indexer_for_left_unsealed_table_from_right_hand_table_with_indexer
TableTests.infer_indexer_from_its_function_return_type TableTests.infer_indexer_from_its_function_return_type
TableTests.infer_indexer_from_its_variable_type_and_unifiable
TableTests.infer_indexer_from_value_property_in_literal TableTests.infer_indexer_from_value_property_in_literal
TableTests.infer_type_when_indexing_from_a_table_indexer
TableTests.inferred_return_type_of_free_table TableTests.inferred_return_type_of_free_table
TableTests.instantiate_table_cloning_3 TableTests.instantiate_table_cloning_3
TableTests.instantiate_tables_at_scope_level TableTests.instantiate_tables_at_scope_level
TableTests.invariant_table_properties_means_instantiating_tables_in_assignment_is_unsound TableTests.invariant_table_properties_means_instantiating_tables_in_assignment_is_unsound
TableTests.invariant_table_properties_means_instantiating_tables_in_call_is_unsound TableTests.invariant_table_properties_means_instantiating_tables_in_call_is_unsound
TableTests.leaking_bad_metatable_errors TableTests.length_operator_intersection
TableTests.length_operator_non_table_union
TableTests.length_operator_union
TableTests.less_exponential_blowup_please TableTests.less_exponential_blowup_please
TableTests.meta_add_both_ways
TableTests.metatable_mismatch_should_fail TableTests.metatable_mismatch_should_fail
TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred
TableTests.mixed_tables_with_implicit_numbered_keys TableTests.mixed_tables_with_implicit_numbered_keys
@ -330,6 +366,8 @@ TableTests.quantify_even_that_table_was_never_exported_at_all
TableTests.quantify_metatables_of_metatables_of_table TableTests.quantify_metatables_of_metatables_of_table
TableTests.reasonable_error_when_adding_a_nonexistent_property_to_an_array_like_table TableTests.reasonable_error_when_adding_a_nonexistent_property_to_an_array_like_table
TableTests.recursive_metatable_type_call TableTests.recursive_metatable_type_call
TableTests.result_is_always_any_if_lhs_is_any
TableTests.result_is_bool_for_equality_operators_if_lhs_is_any
TableTests.right_table_missing_key2 TableTests.right_table_missing_key2
TableTests.scalar_is_a_subtype_of_a_compatible_polymorphic_shape_type TableTests.scalar_is_a_subtype_of_a_compatible_polymorphic_shape_type
TableTests.scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type TableTests.scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type
@ -341,6 +379,7 @@ TableTests.shared_selfs_through_metatables
TableTests.subproperties_can_also_be_covariantly_tested TableTests.subproperties_can_also_be_covariantly_tested
TableTests.table_call_metamethod_basic TableTests.table_call_metamethod_basic
TableTests.table_call_metamethod_generic TableTests.table_call_metamethod_generic
TableTests.table_call_metamethod_must_be_callable
TableTests.table_function_check_use_after_free TableTests.table_function_check_use_after_free
TableTests.table_insert_should_cope_with_optional_properties_in_strict TableTests.table_insert_should_cope_with_optional_properties_in_strict
TableTests.table_param_width_subtyping_1 TableTests.table_param_width_subtyping_1
@ -354,6 +393,7 @@ TableTests.table_unification_4
TableTests.table_unifies_into_map TableTests.table_unifies_into_map
TableTests.top_table_type TableTests.top_table_type
TableTests.type_mismatch_on_massive_table_is_cut_short TableTests.type_mismatch_on_massive_table_is_cut_short
TableTests.unification_of_unions_in_a_self_referential_type
TableTests.unifying_tables_shouldnt_uaf1 TableTests.unifying_tables_shouldnt_uaf1
TableTests.used_colon_instead_of_dot TableTests.used_colon_instead_of_dot
TableTests.used_dot_instead_of_colon TableTests.used_dot_instead_of_colon
@ -362,11 +402,16 @@ ToDot.function
ToString.exhaustive_toString_of_cyclic_table ToString.exhaustive_toString_of_cyclic_table
ToString.free_types ToString.free_types
ToString.named_metatable_toStringNamedFunction ToString.named_metatable_toStringNamedFunction
ToString.no_parentheses_around_cyclic_function_type_in_union
ToString.pick_distinct_names_for_mixed_explicit_and_implicit_generics ToString.pick_distinct_names_for_mixed_explicit_and_implicit_generics
ToString.primitive
ToString.tostring_error_mismatch ToString.tostring_error_mismatch
ToString.tostring_unsee_ttv_if_array
ToString.toStringDetailed2 ToString.toStringDetailed2
ToString.toStringErrorPack ToString.toStringErrorPack
ToString.toStringNamedFunction_generic_pack ToString.toStringNamedFunction_generic_pack
ToString.toStringNamedFunction_map
TranspilerTests.types_should_not_be_considered_cyclic_if_they_are_not_recursive
TryUnifyTests.members_of_failed_typepack_unification_are_unified_with_errorType TryUnifyTests.members_of_failed_typepack_unification_are_unified_with_errorType
TryUnifyTests.result_of_failed_typepack_unification_is_constrained TryUnifyTests.result_of_failed_typepack_unification_is_constrained
TryUnifyTests.typepack_unification_should_trim_free_tails TryUnifyTests.typepack_unification_should_trim_free_tails
@ -374,6 +419,9 @@ TryUnifyTests.uninhabited_table_sub_anything
TryUnifyTests.uninhabited_table_sub_never TryUnifyTests.uninhabited_table_sub_never
TryUnifyTests.variadics_should_use_reversed_properly TryUnifyTests.variadics_should_use_reversed_properly
TypeAliases.alias_expands_to_bare_reference_to_imported_type TypeAliases.alias_expands_to_bare_reference_to_imported_type
TypeAliases.corecursive_types_generic
TypeAliases.cyclic_function_type_in_type_alias
TypeAliases.cyclic_types_of_named_table_fields_do_not_expand_when_stringified
TypeAliases.dont_lose_track_of_PendingExpansionTypes_after_substitution TypeAliases.dont_lose_track_of_PendingExpansionTypes_after_substitution
TypeAliases.free_variables_from_typeof_in_aliases TypeAliases.free_variables_from_typeof_in_aliases
TypeAliases.generic_param_remap TypeAliases.generic_param_remap
@ -385,6 +433,7 @@ TypeAliases.mutually_recursive_types_restriction_not_ok_2
TypeAliases.mutually_recursive_types_swapsies_not_ok TypeAliases.mutually_recursive_types_swapsies_not_ok
TypeAliases.recursive_types_restriction_not_ok TypeAliases.recursive_types_restriction_not_ok
TypeAliases.report_shadowed_aliases TypeAliases.report_shadowed_aliases
TypeAliases.saturate_to_first_type_pack
TypeAliases.stringify_optional_parameterized_alias TypeAliases.stringify_optional_parameterized_alias
TypeAliases.type_alias_local_mutation TypeAliases.type_alias_local_mutation
TypeAliases.type_alias_local_rename TypeAliases.type_alias_local_rename
@ -404,10 +453,10 @@ TypeInfer.check_expr_recursion_limit
TypeInfer.check_type_infer_recursion_count TypeInfer.check_type_infer_recursion_count
TypeInfer.cli_39932_use_unifier_in_ensure_methods TypeInfer.cli_39932_use_unifier_in_ensure_methods
TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error
TypeInfer.dont_ice_when_failing_the_occurs_check
TypeInfer.dont_report_type_errors_within_an_AstExprError TypeInfer.dont_report_type_errors_within_an_AstExprError
TypeInfer.dont_report_type_errors_within_an_AstStatError TypeInfer.dont_report_type_errors_within_an_AstStatError
TypeInfer.follow_on_new_types_in_substitution TypeInfer.follow_on_new_types_in_substitution
TypeInfer.fuzz_free_table_type_change_during_index_check
TypeInfer.globals_are_banned_in_strict_mode TypeInfer.globals_are_banned_in_strict_mode
TypeInfer.if_statement TypeInfer.if_statement
TypeInfer.infer_locals_via_assignment_from_its_call_site TypeInfer.infer_locals_via_assignment_from_its_call_site
@ -419,12 +468,17 @@ TypeInfer.no_stack_overflow_from_isoptional
TypeInfer.promote_tail_type_packs TypeInfer.promote_tail_type_packs
TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter
TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2 TypeInfer.recursive_function_that_invokes_itself_with_a_refinement_of_its_parameter_2
TypeInfer.stringify_nested_unions_with_optionals
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
TypeInfer.tc_error TypeInfer.tc_error
TypeInfer.tc_error_2 TypeInfer.tc_error_2
TypeInfer.tc_if_else_expressions_expected_type_3 TypeInfer.tc_if_else_expressions_expected_type_3
TypeInfer.type_infer_recursion_limit_no_ice TypeInfer.type_infer_recursion_limit_no_ice
TypeInfer.type_infer_recursion_limit_normalizer TypeInfer.type_infer_recursion_limit_normalizer
TypeInfer.unify_nearly_identical_recursive_types
TypeInferAnyError.any_type_propagates
TypeInferAnyError.assign_prop_to_table_by_calling_any_yields_any
TypeInferAnyError.call_to_any_yields_any
TypeInferAnyError.can_subscript_any TypeInferAnyError.can_subscript_any
TypeInferAnyError.for_in_loop_iterator_is_any TypeInferAnyError.for_in_loop_iterator_is_any
TypeInferAnyError.for_in_loop_iterator_is_any2 TypeInferAnyError.for_in_loop_iterator_is_any2
@ -433,19 +487,25 @@ TypeInferAnyError.for_in_loop_iterator_is_error2
TypeInferAnyError.for_in_loop_iterator_returns_any TypeInferAnyError.for_in_loop_iterator_returns_any
TypeInferAnyError.for_in_loop_iterator_returns_any2 TypeInferAnyError.for_in_loop_iterator_returns_any2
TypeInferAnyError.intersection_of_any_can_have_props TypeInferAnyError.intersection_of_any_can_have_props
TypeInferAnyError.quantify_any_does_not_bind_to_itself
TypeInferAnyError.replace_every_free_type_when_unifying_a_complex_function_with_any TypeInferAnyError.replace_every_free_type_when_unifying_a_complex_function_with_any
TypeInferAnyError.type_error_addition TypeInferAnyError.type_error_addition
TypeInferClasses.callable_classes TypeInferClasses.callable_classes
TypeInferClasses.can_read_prop_of_base_class_using_string TypeInferClasses.can_read_prop_of_base_class_using_string
TypeInferClasses.cannot_unify_class_instance_with_primitive TypeInferClasses.cannot_unify_class_instance_with_primitive
TypeInferClasses.class_type_mismatch_with_name_conflict TypeInferClasses.class_type_mismatch_with_name_conflict
TypeInferClasses.class_unification_type_mismatch_is_correct_order
TypeInferClasses.detailed_class_unification_error TypeInferClasses.detailed_class_unification_error
TypeInferClasses.index_instance_property TypeInferClasses.index_instance_property
TypeInferClasses.indexable_classes TypeInferClasses.indexable_classes
TypeInferClasses.optional_class_field_access_error
TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties
TypeInferClasses.table_indexers_are_invariant TypeInferClasses.table_indexers_are_invariant
TypeInferClasses.type_mismatch_invariance_required_for_error TypeInferClasses.type_mismatch_invariance_required_for_error
TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class TypeInferClasses.we_can_report_when_someone_is_trying_to_use_a_table_rather_than_a_class
TypeInferFunctions.another_higher_order_function
TypeInferFunctions.another_other_higher_order_function
TypeInferFunctions.another_recursive_local_function
TypeInferFunctions.apply_of_lambda_with_inferred_and_explicit_types TypeInferFunctions.apply_of_lambda_with_inferred_and_explicit_types
TypeInferFunctions.cannot_hoist_interior_defns_into_signature TypeInferFunctions.cannot_hoist_interior_defns_into_signature
TypeInferFunctions.check_function_bodies TypeInferFunctions.check_function_bodies
@ -478,9 +538,10 @@ TypeInferFunctions.infer_generic_lib_function_function_argument
TypeInferFunctions.infer_return_type_from_selected_overload TypeInferFunctions.infer_return_type_from_selected_overload
TypeInferFunctions.infer_return_value_type TypeInferFunctions.infer_return_value_type
TypeInferFunctions.infer_that_function_does_not_return_a_table TypeInferFunctions.infer_that_function_does_not_return_a_table
TypeInferFunctions.inferred_higher_order_functions_are_quantified_at_the_right_time3
TypeInferFunctions.instantiated_type_packs_must_have_a_non_null_scope TypeInferFunctions.instantiated_type_packs_must_have_a_non_null_scope
TypeInferFunctions.it_is_ok_to_oversaturate_a_higher_order_function_argument TypeInferFunctions.it_is_ok_to_oversaturate_a_higher_order_function_argument
TypeInferFunctions.list_all_overloads_if_no_overload_takes_given_argument_count
TypeInferFunctions.list_only_alternative_overloads_that_match_argument_count
TypeInferFunctions.luau_subtyping_is_np_hard TypeInferFunctions.luau_subtyping_is_np_hard
TypeInferFunctions.no_lossy_function_type TypeInferFunctions.no_lossy_function_type
TypeInferFunctions.num_is_solved_after_num_or_str TypeInferFunctions.num_is_solved_after_num_or_str
@ -490,6 +551,7 @@ TypeInferFunctions.other_things_are_not_related_to_function
TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible
TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2 TypeInferFunctions.param_1_and_2_both_takes_the_same_generic_but_their_arguments_are_incompatible_2
TypeInferFunctions.record_matching_overload TypeInferFunctions.record_matching_overload
TypeInferFunctions.recursive_local_function
TypeInferFunctions.report_exiting_without_return_strict TypeInferFunctions.report_exiting_without_return_strict
TypeInferFunctions.return_type_by_overload TypeInferFunctions.return_type_by_overload
TypeInferFunctions.too_few_arguments_variadic TypeInferFunctions.too_few_arguments_variadic
@ -501,6 +563,10 @@ TypeInferFunctions.too_many_return_values_no_function
TypeInferFunctions.vararg_function_is_quantified TypeInferFunctions.vararg_function_is_quantified
TypeInferLoops.cli_68448_iterators_need_not_accept_nil TypeInferLoops.cli_68448_iterators_need_not_accept_nil
TypeInferLoops.dcr_iteration_explore_raycast_minimization TypeInferLoops.dcr_iteration_explore_raycast_minimization
TypeInferLoops.dcr_iteration_fragmented_keys
TypeInferLoops.dcr_iteration_minimized_fragmented_keys_1
TypeInferLoops.dcr_iteration_minimized_fragmented_keys_2
TypeInferLoops.dcr_iteration_minimized_fragmented_keys_3
TypeInferLoops.dcr_iteration_on_never_gives_never TypeInferLoops.dcr_iteration_on_never_gives_never
TypeInferLoops.dcr_xpath_candidates TypeInferLoops.dcr_xpath_candidates
TypeInferLoops.for_in_loop TypeInferLoops.for_in_loop
@ -511,6 +577,7 @@ TypeInferLoops.for_in_loop_on_non_function
TypeInferLoops.for_in_loop_with_custom_iterator TypeInferLoops.for_in_loop_with_custom_iterator
TypeInferLoops.for_in_loop_with_incompatible_args_to_iterator TypeInferLoops.for_in_loop_with_incompatible_args_to_iterator
TypeInferLoops.for_in_loop_with_next TypeInferLoops.for_in_loop_with_next
TypeInferLoops.for_in_with_an_iterator_of_type_any
TypeInferLoops.for_in_with_generic_next TypeInferLoops.for_in_with_generic_next
TypeInferLoops.for_in_with_just_one_iterator_is_ok TypeInferLoops.for_in_with_just_one_iterator_is_ok
TypeInferLoops.for_loop TypeInferLoops.for_loop
@ -530,13 +597,17 @@ TypeInferLoops.unreachable_code_after_infinite_loop
TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free
TypeInferLoops.while_loop TypeInferLoops.while_loop
TypeInferModules.bound_free_table_export_is_ok TypeInferModules.bound_free_table_export_is_ok
TypeInferModules.do_not_modify_imported_types
TypeInferModules.do_not_modify_imported_types_4 TypeInferModules.do_not_modify_imported_types_4
TypeInferModules.do_not_modify_imported_types_5 TypeInferModules.do_not_modify_imported_types_5
TypeInferModules.general_require_call_expression TypeInferModules.general_require_call_expression
TypeInferModules.module_type_conflict TypeInferModules.module_type_conflict
TypeInferModules.module_type_conflict_instantiated TypeInferModules.module_type_conflict_instantiated
TypeInferModules.require
TypeInferModules.require_failed_module TypeInferModules.require_failed_module
TypeInferOOP.CheckMethodsOfSealed
TypeInferOOP.cycle_between_object_constructor_and_alias TypeInferOOP.cycle_between_object_constructor_and_alias
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_another_overload_works
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2 TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_it_wont_help_2
TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon TypeInferOOP.dont_suggest_using_colon_rather_than_dot_if_not_defined_with_colon
TypeInferOOP.inferring_hundreds_of_self_calls_should_not_suffocate_memory TypeInferOOP.inferring_hundreds_of_self_calls_should_not_suffocate_memory
@ -555,33 +626,37 @@ TypeInferOperators.compound_assign_result_must_be_compatible_with_var
TypeInferOperators.concat_op_on_free_lhs_and_string_rhs TypeInferOperators.concat_op_on_free_lhs_and_string_rhs
TypeInferOperators.concat_op_on_string_lhs_and_free_rhs TypeInferOperators.concat_op_on_string_lhs_and_free_rhs
TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops
TypeInferOperators.luau-polyfill.Array.startswith TypeInferOperators.equality_operations_succeed_if_any_union_branch_succeeds
TypeInferOperators.error_on_invalid_operand_types_to_relational_operators2
TypeInferOperators.luau_polyfill_is_array TypeInferOperators.luau_polyfill_is_array
TypeInferOperators.normalize_strings_comparison TypeInferOperators.mm_comparisons_must_return_a_boolean
TypeInferOperators.operator_eq_completely_incompatible
TypeInferOperators.operator_eq_verifies_types_do_intersect TypeInferOperators.operator_eq_verifies_types_do_intersect
TypeInferOperators.reducing_and TypeInferOperators.reducing_and
TypeInferOperators.refine_and_or TypeInferOperators.refine_and_or
TypeInferOperators.reworked_and
TypeInferOperators.reworked_or
TypeInferOperators.strict_binary_op_where_lhs_unknown TypeInferOperators.strict_binary_op_where_lhs_unknown
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs
TypeInferOperators.typecheck_unary_len_error TypeInferOperators.typecheck_unary_len_error
TypeInferOperators.typecheck_unary_minus TypeInferOperators.typecheck_unary_minus
TypeInferOperators.typecheck_unary_minus_error TypeInferOperators.typecheck_unary_minus_error
TypeInferOperators.unrelated_classes_cannot_be_compared
TypeInferOperators.unrelated_primitives_cannot_be_compared
TypeInferPrimitives.CheckMethodsOfNumber TypeInferPrimitives.CheckMethodsOfNumber
TypeInferPrimitives.string_function_indirect
TypeInferPrimitives.string_index TypeInferPrimitives.string_index
TypeInferUnknownNever.array_like_table_of_never_is_inhabitable TypeInferUnknownNever.array_like_table_of_never_is_inhabitable
TypeInferUnknownNever.assign_to_prop_which_is_never
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_never TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_never
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_never TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_never
TypeInferUnknownNever.length_of_never TypeInferUnknownNever.length_of_never
TypeInferUnknownNever.math_operators_and_never TypeInferUnknownNever.math_operators_and_never
TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable
TypePackTests.fuzz_typepack_iter_follow_2 TypePackTests.fuzz_typepack_iter_follow_2
TypePackTests.pack_tail_unification_check TypePackTests.pack_tail_unification_check
TypePackTests.parenthesized_varargs_returns_any TypePackTests.parenthesized_varargs_returns_any
TypePackTests.type_alias_backwards_compatible TypePackTests.type_alias_backwards_compatible
TypePackTests.type_alias_default_type_errors TypePackTests.type_alias_default_type_errors
TypePackTests.type_alias_type_packs_import
TypePackTests.type_packs_with_tails_in_vararg_adjustment TypePackTests.type_packs_with_tails_in_vararg_adjustment
TypePackTests.unify_variadic_tails_in_arguments TypePackTests.unify_variadic_tails_in_arguments
TypePackTests.unify_variadic_tails_in_arguments_free TypePackTests.unify_variadic_tails_in_arguments_free
@ -592,7 +667,7 @@ TypeSingletons.error_detailed_tagged_union_mismatch_string
TypeSingletons.function_args_infer_singletons TypeSingletons.function_args_infer_singletons
TypeSingletons.function_call_with_singletons TypeSingletons.function_call_with_singletons
TypeSingletons.function_call_with_singletons_mismatch TypeSingletons.function_call_with_singletons_mismatch
TypeSingletons.overloaded_function_call_with_singletons TypeSingletons.overloaded_function_call_with_singletons_mismatch
TypeSingletons.return_type_of_f_is_not_widened TypeSingletons.return_type_of_f_is_not_widened
TypeSingletons.table_properties_singleton_strings TypeSingletons.table_properties_singleton_strings
TypeSingletons.table_properties_type_error_escapes TypeSingletons.table_properties_type_error_escapes
@ -605,12 +680,24 @@ UnionTypes.error_detailed_union_all
UnionTypes.error_detailed_union_part UnionTypes.error_detailed_union_part
UnionTypes.generic_function_with_optional_arg UnionTypes.generic_function_with_optional_arg
UnionTypes.index_on_a_union_type_with_missing_property UnionTypes.index_on_a_union_type_with_missing_property
UnionTypes.index_on_a_union_type_with_mixed_types
UnionTypes.index_on_a_union_type_with_one_optional_property
UnionTypes.index_on_a_union_type_with_one_property_of_type_any
UnionTypes.index_on_a_union_type_with_property_guaranteed_to_exist
UnionTypes.index_on_a_union_type_works_at_arbitrary_depth
UnionTypes.less_greedy_unification_with_union_types UnionTypes.less_greedy_unification_with_union_types
UnionTypes.optional_arguments_table2 UnionTypes.optional_arguments_table2
UnionTypes.optional_assignment_errors
UnionTypes.optional_call_error
UnionTypes.optional_field_access_error
UnionTypes.optional_index_error UnionTypes.optional_index_error
UnionTypes.optional_length_error UnionTypes.optional_length_error
UnionTypes.optional_missing_key_error_details
UnionTypes.optional_union_follow
UnionTypes.optional_union_functions
UnionTypes.optional_union_members
UnionTypes.optional_union_methods
UnionTypes.table_union_write_indirect UnionTypes.table_union_write_indirect
UnionTypes.unify_sealed_table_union_check
UnionTypes.union_of_functions UnionTypes.union_of_functions
UnionTypes.union_of_functions_mentioning_generic_typepacks UnionTypes.union_of_functions_mentioning_generic_typepacks
UnionTypes.union_of_functions_mentioning_generics UnionTypes.union_of_functions_mentioning_generics
@ -620,3 +707,4 @@ UnionTypes.union_of_functions_with_mismatching_result_arities
UnionTypes.union_of_functions_with_mismatching_result_variadics UnionTypes.union_of_functions_with_mismatching_result_variadics
UnionTypes.union_of_functions_with_variadics UnionTypes.union_of_functions_with_variadics
UnionTypes.union_true_and_false UnionTypes.union_true_and_false
VisitType.throw_when_limit_is_exceeded