Sync to upstream/release/599 (#1069)

## What's Changed

- Improve POSIX compliance in `CLI/FileUtils.cpp` by @SamuraiCrow #1064
- `AstStat*::hasEnd` is deprecated; use `AstStatBlock::hasEnd` instead
- Added a lint for common misuses of the `#` operator
- Luau now issues deprecated diagnostics for some uses of `getfenv` and
`setfenv`
- Fixed a case where we included a trailing space in some error
stringifications

### Compiler

- Do not do further analysis in O2 on self functions
- Improve detection of invalid repeat..until expressions vs continue

### New Type Solver

- We now cache subtype test results to improve performance
- Improved operator inference mechanics (aka type families)
- Further work towards type states
- Work towards [new non-strict
mode](https://github.com/Roblox/luau/blob/master/rfcs/new-nonstrict.md)
continues

### Native Codegen

- Instruction last use locations should follow the order in which blocks
are lowered
- Add a bonus assertion to IrLoweringA64::tempAddr

---

Co-authored-by: Arseny Kapoulkine <arseny.kapoulkine@gmail.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
Co-authored-by: Aaron Weiss <aaronweiss@roblox.com>
Co-authored-by: Alexander McCord <amccord@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
This commit is contained in:
Lily Brown 2023-10-13 13:20:12 -07:00 committed by GitHub
parent 5c94984935
commit 24fdac4c05
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
59 changed files with 2432 additions and 945 deletions

View file

@ -69,6 +69,12 @@ struct ConstraintGraphBuilder
// This is null when the CGB is initially constructed.
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.
std::vector<ConstraintPtr> constraints;
@ -205,8 +211,6 @@ private:
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::vector<TypeId> checkLValues(const ScopePtr& scope, AstArray<AstExpr*> exprs);
TypeId checkLValue(const ScopePtr& scope, AstExpr* expr);
TypeId checkLValue(const ScopePtr& scope, AstExprLocal* local);
TypeId checkLValue(const ScopePtr& scope, AstExprGlobal* global);
@ -303,6 +307,8 @@ private:
*/
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
* For example, calling a function with annotation ((number) -> string & ((string) -> number))
* 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, AstExprError* error);
void visitLValue(DfgScope* scope, AstExpr* e);
void visitLValue(DfgScope* scope, AstExprLocal* l);
void visitLValue(DfgScope* scope, AstExprGlobal* g);
void visitLValue(DfgScope* scope, AstExprIndexName* i);
void visitLValue(DfgScope* scope, AstExprIndexExpr* i);
void visitLValue(DfgScope* scope, AstExprError* e);
void visitLValue(DfgScope* scope, AstExpr* e, BreadcrumbId bc);
void visitLValue(DfgScope* scope, AstExprLocal* l, BreadcrumbId bc);
void visitLValue(DfgScope* scope, AstExprGlobal* g, BreadcrumbId bc);
void visitLValue(DfgScope* scope, AstExprIndexName* i, BreadcrumbId bc);
void visitLValue(DfgScope* scope, AstExprIndexExpr* i, BreadcrumbId bc);
void visitLValue(DfgScope* scope, AstExprError* e, BreadcrumbId bc);
void visitType(DfgScope* scope, AstType* t);
void visitType(DfgScope* scope, AstTypeReference* r);

View file

@ -358,13 +358,23 @@ struct PackWhereClauseNeeded
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,
DuplicateTypeDefinition, CountMismatch, FunctionDoesNotTakeSelf, FunctionRequiresSelf, OccursCheckFailed, UnknownRequire,
IncorrectGenericParameterCount, SyntaxError, CodeTooComplex, UnificationTooComplex, UnknownPropButFoundLikeProp, GenericError, InternalError,
CannotCallNonFunction, ExtraInformation, DeprecatedApiUsed, ModuleHasCyclicDependency, IllegalRequire, FunctionExitsWithoutReturning,
DuplicateGenericParameter, CannotInferBinaryOperation, MissingProperties, SwappedGenericTypeParameter, OptionalValueAccess, MissingUnionProperty,
TypesAreUnrelated, NormalizationTooComplex, TypePackMismatch, DynamicPropertyLookupOnClassesUnsafe, UninhabitedTypeFamily,
UninhabitedTypePackFamily, WhereClauseNeeded, PackWhereClauseNeeded>;
UninhabitedTypePackFamily, WhereClauseNeeded, PackWhereClauseNeeded, CheckedFunctionCallError>;
struct TypeErrorSummary
{

View file

@ -245,12 +245,12 @@ public:
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,
const ScopePtr& globalScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options,
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,
const ScopePtr& globalScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options,
TypeCheckLimits limits, bool recordJsonLog);

View file

@ -3,13 +3,17 @@
#include "Luau/Module.h"
#include "Luau/NotNull.h"
#include "Luau/DataFlowGraph.h"
namespace Luau
{
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

View file

@ -3,6 +3,7 @@
#include "Luau/Type.h"
#include "Luau/TypePack.h"
#include "Luau/TypePairHash.h"
#include "Luau/UnifierSharedState.h"
#include <vector>
@ -28,6 +29,7 @@ struct SubtypingResult
bool isSubtype = false;
bool isErrorSuppressing = false;
bool normalizationTooComplex = false;
bool isCacheable = true;
SubtypingResult& andAlso(const SubtypingResult& other);
SubtypingResult& orElse(const SubtypingResult& other);
@ -38,6 +40,24 @@ struct SubtypingResult
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
{
NotNull<BuiltinTypes> builtinTypes;
@ -55,29 +75,25 @@ struct Subtyping
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>;
SeenSet seenTypes;
Subtyping(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> typeArena, NotNull<Normalizer> normalizer,
NotNull<InternalErrorReporter> iceReporter, NotNull<Scope> scope);
Subtyping(const Subtyping&) = delete;
Subtyping& operator=(const Subtyping&) = delete;
Subtyping(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 cyclic types
// TODO recursion limits
@ -86,58 +102,63 @@ struct Subtyping
SubtypingResult isSubtype(TypePackId subTy, TypePackId superTy);
private:
SubtypingResult isCovariantWith(TypeId subTy, TypeId superTy);
SubtypingResult isCovariantWith(TypePackId subTy, TypePackId superTy);
DenseHashMap<std::pair<TypeId, TypeId>, SubtypingResult, TypePairHash> resultCache{{}};
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>
SubtypingResult isContravariantWith(SubTy&& subTy, SuperTy&& superTy);
SubtypingResult isContravariantWith(SubtypingEnvironment& env, SubTy&& subTy, SuperTy&& 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>
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>
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>
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(const UnionType* subUnion, TypeId superTy);
SubtypingResult isCovariantWith(TypeId subTy, const IntersectionType* superIntersection);
SubtypingResult isCovariantWith(const IntersectionType* subIntersection, TypeId superTy);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const UnionType* superUnion);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const UnionType* subUnion, TypeId superTy);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, TypeId subTy, const IntersectionType* superIntersection);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const IntersectionType* subIntersection, TypeId superTy);
SubtypingResult isCovariantWith(const NegationType* subNegation, TypeId superTy);
SubtypingResult isCovariantWith(const TypeId subTy, const NegationType* superNegation);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NegationType* subNegation, TypeId superTy);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TypeId subTy, const NegationType* superNegation);
SubtypingResult isCovariantWith(const PrimitiveType* subPrim, const PrimitiveType* superPrim);
SubtypingResult isCovariantWith(const SingletonType* subSingleton, const PrimitiveType* superPrim);
SubtypingResult isCovariantWith(const SingletonType* subSingleton, const SingletonType* superSingleton);
SubtypingResult isCovariantWith(const TableType* subTable, const TableType* superTable);
SubtypingResult isCovariantWith(const MetatableType* subMt, const MetatableType* superMt);
SubtypingResult isCovariantWith(const MetatableType* subMt, const TableType* superTable);
SubtypingResult isCovariantWith(const ClassType* subClass, const ClassType* superClass);
SubtypingResult isCovariantWith(const ClassType* subClass, const TableType* superTable);
SubtypingResult isCovariantWith(const FunctionType* subFunction, const FunctionType* superFunction);
SubtypingResult isCovariantWith(const PrimitiveType* subPrim, const TableType* superTable);
SubtypingResult isCovariantWith(const SingletonType* subSingleton, const TableType* superTable);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const PrimitiveType* superPrim);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const PrimitiveType* superPrim);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const SingletonType* superSingleton);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const TableType* subTable, const TableType* superTable);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const MetatableType* superMt);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const MetatableType* subMt, const TableType* superTable);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const ClassType* superClass);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const TableType* superTable);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const FunctionType* subFunction, const FunctionType* superFunction);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, 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(const NormalizedClassType& subClass, const NormalizedClassType& superClass);
SubtypingResult isCovariantWith(const NormalizedClassType& subClass, const TypeIds& superTables);
SubtypingResult isCovariantWith(const NormalizedStringType& subString, const NormalizedStringType& superString);
SubtypingResult isCovariantWith(const NormalizedStringType& subString, const TypeIds& superTables);
SubtypingResult isCovariantWith(const NormalizedFunctionType& subFunction, const NormalizedFunctionType& superFunction);
SubtypingResult isCovariantWith(const TypeIds& subTypes, const TypeIds& superTypes);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedType* subNorm, const NormalizedType* superNorm);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const NormalizedClassType& superClass);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedClassType& subClass, const TypeIds& superTables);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedStringType& subString, const NormalizedStringType& superString);
SubtypingResult isCovariantWith(SubtypingEnvironment& env, const NormalizedStringType& subString, const TypeIds& superTables);
SubtypingResult isCovariantWith(
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(TypePackId subTp, TypePackId superTp);
bool bindGeneric(SubtypingEnvironment& env, TypeId subTp, TypeId superTp);
bool bindGeneric(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp);
template<typename T, typename Container>
TypeId makeAggregateType(const Container& container, TypeId orElse);

View file

@ -93,7 +93,7 @@ struct TypeFamily
std::string name;
/// 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
@ -105,7 +105,7 @@ struct TypePackFamily
std::string name;
/// 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
@ -149,6 +149,8 @@ struct BuiltinTypeFamilies
{
BuiltinTypeFamilies();
TypeFamily notFamily;
TypeFamily addFamily;
TypeFamily subFamily;
TypeFamily mulFamily;
@ -157,9 +159,15 @@ struct BuiltinTypeFamilies
TypeFamily powFamily;
TypeFamily modFamily;
TypeFamily concatFamily;
TypeFamily andFamily;
TypeFamily orFamily;
TypeFamily ltFamily;
TypeFamily leFamily;
TypeFamily eqFamily;
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/NotNull.h"
#include "Type.h"
#include "TypePairHash.h"
#include "TypeCheckLimits.h"
#include "TypeChecker2.h"
@ -29,29 +30,6 @@ enum class OccursCheckResult
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
{
NotNull<TypeArena> arena;

View file

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

View file

@ -12,8 +12,9 @@
#include <unordered_set>
#include <utility>
LUAU_FASTFLAG(DebugLuauReadWriteProperties)
LUAU_FASTFLAGVARIABLE(LuauAutocompleteDoEnd, false)
LUAU_FASTFLAG(DebugLuauReadWriteProperties);
LUAU_FASTFLAG(LuauClipExtraHasEndProps);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteDoEnd, false);
LUAU_FASTFLAGVARIABLE(LuauAutocompleteStringLiteralBounds, false);
static const std::unordered_set<std::string> kStatementStartingKeywords = {
@ -1055,17 +1056,29 @@ static AutocompleteEntryMap autocompleteStatement(
for (const auto& kw : kStatementStartingKeywords)
result.emplace(kw, AutocompleteEntry{AutocompleteEntryKind::Keyword});
if (FFlag::LuauClipExtraHasEndProps)
{
for (auto it = ancestry.rbegin(); it != ancestry.rend(); ++it)
{
if (AstStatForIn* statForIn = (*it)->as<AstStatForIn>(); statForIn && !statForIn->hasEnd)
if (AstStatForIn* statForIn = (*it)->as<AstStatForIn>(); statForIn && !statForIn->body->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstStatFor* statFor = (*it)->as<AstStatFor>(); statFor && !statFor->hasEnd)
else if (AstStatFor* statFor = (*it)->as<AstStatFor>(); statFor && !statFor->body->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
else if (AstStatIf* statIf = (*it)->as<AstStatIf>(); statIf && !statIf->hasEnd)
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->hasEnd)
}
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->hasEnd)
else if (AstExprFunction* exprFunction = (*it)->as<AstExprFunction>(); exprFunction && !exprFunction->body->hasEnd)
result.emplace("end", AutocompleteEntry{AutocompleteEntryKind::Keyword});
if (FFlag::LuauAutocompleteDoEnd)
{
@ -1073,6 +1086,28 @@ static AutocompleteEntryMap autocompleteStatement(
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});
}
}
}
if (ancestry.size() >= 2)
{
@ -1086,9 +1121,17 @@ static AutocompleteEntryMap autocompleteStatement(
}
}
if (AstStatRepeat* statRepeat = parent->as<AstStatRepeat>(); statRepeat && !statRepeat->hasUntil)
if (FFlag::LuauClipExtraHasEndProps)
{
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)
{
@ -1101,8 +1144,16 @@ static AutocompleteEntryMap autocompleteStatement(
}
}
if (AstStatRepeat* statRepeat = extractStat<AstStatRepeat>(ancestry); statRepeat && !statRepeat->hasUntil)
if (FFlag::LuauClipExtraHasEndProps)
{
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;
}

View file

@ -175,6 +175,8 @@ void ConstraintGraphBuilder::visitModuleRoot(AstStatBlock* block)
visitBlockWithoutChildScope(scope, block);
fillInInferredBindings(scope, block);
if (logger)
logger->captureGenerationModule(module);
}
@ -586,89 +588,40 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStat* stat)
ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* local)
{
std::vector<TypeId> varTypes;
std::vector<std::optional<TypeId>> varTypes;
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,
// for the purpose of synthetic name attribution.
std::optional<TypeId> firstValueType;
for (AstLocal* local : local->vars)
{
TypeId ty = nullptr;
TypeId assignee = arena->addType(BlockedType{});
assignees.push_back(assignee);
if (!firstValueType)
firstValueType = assignee;
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;
TypeId annotationTy = resolveType(scope, local->annotation, /* inTypeArguments */ false);
varTypes.push_back(annotationTy);
if (value->is<AstExprConstantNil>())
{
// HACK: we leave nil-initialized things floating under the
// assumption that they will later be populated.
//
// See the test TypeInfer/infer_locals_with_nil_value. Better flow
// awareness should make this obsolete.
if (i < varTypes.size() && !varTypes[i])
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;
addConstraint(scope, local->location, SubtypeConstraint{assignee, annotationTy});
}
else
{
std::vector<std::optional<TypeId>> expectedTypes;
if (hasAnnotation)
expectedTypes.insert(begin(expectedTypes), begin(varTypes) + i, end(varTypes));
varTypes.push_back(std::nullopt);
TypePackId exprPack = checkPack(scope, value, expectedTypes, /*generalize*/ true).tp;
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{});
}
BreadcrumbId bc = dfg->getBreadcrumb(local);
scope->lvalueTypes[bc->def] = assignee;
}
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)
{
@ -691,16 +644,16 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l
AstLocal* l = local->vars.data[i];
Location location = l->location;
if (!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.
std::optional<TypeId> annotation = varTypes[i];
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)
@ -712,33 +665,35 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* l
if (!call)
continue;
if (auto maybeRequire = matchRequire(*call))
{
auto maybeRequire = matchRequire(*call);
if (!maybeRequire)
continue;
AstExpr* require = *maybeRequire;
if (auto moduleInfo = moduleResolver->resolveModuleInfo(module->name, *require))
{
const Name name{local->vars.data[i]->name.value};
auto moduleInfo = moduleResolver->resolveModuleInfo(module->name, *require);
if (!moduleInfo)
continue;
if (ModulePtr module = moduleResolver->getModule(moduleInfo->name))
{
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)
{
if (!path.empty() && path.front() == moduleInfo->name)
{
if (path.empty() || path.front() != moduleInfo->name)
continue;
for (auto& [name, tf] : scope->importedTypeBindings[name])
tf = TypeFun{{}, {}, builtinTypes->anyType};
}
}
}
}
}
}
}
return ControlFlow::None;
}
@ -781,6 +736,7 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* f
std::vector<TypeId> variableTypes;
variableTypes.reserve(forIn->vars.size);
for (AstLocal* var : forIn->vars)
{
TypeId ty = nullptr;
@ -790,18 +746,18 @@ ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatForIn* f
ty = freshType(loopScope);
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);
loopScope->lvalueTypes[bc->def] = ty;
loopScope->rvalueRefinements[bc->def] = ty;
loopScope->lvalueTypes[bc->def] = assignee;
}
// It is always ok to provide too few variables, so we give this pack a free tail.
TypePackId variablePack = arena->addTypePack(std::move(variableTypes), freshTypePack(loopScope));
TypePackId variablePack = arena->addTypePack(std::move(variableTypes));
addConstraint(
loopScope, getLocation(forIn->values), IterableConstraint{iterator, variablePack, forIn->values.data[0], &module->astForInNextTypes});
visit(loopScope, forIn->body);
return ControlFlow::None;
@ -1033,24 +989,30 @@ static void bindFreeType(TypeId a, TypeId b)
ControlFlow ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign)
{
std::vector<TypeId> varTypes = checkLValues(scope, assign->vars);
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);
if (get<FreeType>(ty))
TypeId upperBound = follow(checkLValue(scope, lvalue));
if (get<FreeType>(upperBound))
expectedTypes.push_back(std::nullopt);
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 varPack = arena->addTypePack({varTypes});
addConstraint(scope, assign->location, PackSubtypeConstraint{exprPack, varPack});
TypePackId resultPack = checkPack(scope, assign->values, expectedTypes).tp;
addConstraint(scope, assign->location, UnpackConstraint{arena->addTypePack(std::move(assignees)), resultPack});
return ControlFlow::None;
}
@ -1729,7 +1691,7 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprLocal* loc
if (auto ty = scope->lookup(bc->def))
return Inference{*ty, refinementArena.proposition(bc, builtinTypes->truthyType)};
else
ice->ice("AstExprLocal came before its declaration?");
ice->ice("CGB: AstExprLocal came before its declaration?");
}
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* global)
@ -1837,14 +1799,27 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprFunction*
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprUnary* unary)
{
auto [operandType, refinement] = check(scope, unary->expr);
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)};
}
default:
{
TypeId resultType = arena->addType(BlockedType{});
addConstraint(scope, unary->location, UnaryConstraint{unary->op, operandType, resultType});
if (unary->op == AstExprUnary::Not)
return Inference{resultType, refinementArena.negation(refinement)};
else
return Inference{resultType};
}
}
}
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});
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:
{
TypeId resultType = arena->addType(TypeFamilyInstanceType{
@ -1942,6 +1927,57 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprBinary* bi
addConstraint(scope, binary->location, ReduceConstraint{resultType});
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:
{
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)
{
if (auto local = expr->as<AstExprLocal>())
@ -2201,6 +2226,7 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex
return check(scope, expr).ty;
Symbol sym;
NullableBreadcrumbId bc = nullptr;
std::vector<std::string> segments;
std::vector<AstExpr*> exprs;
@ -2210,11 +2236,13 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex
if (auto global = e->as<AstExprGlobal>())
{
sym = global->name;
bc = dfg->getBreadcrumb(global);
break;
}
else if (auto local = e->as<AstExprLocal>())
{
sym = local->local;
bc = dfg->getBreadcrumb(local);
break;
}
else if (auto indexName = e->as<AstExprIndexName>())
@ -2251,7 +2279,16 @@ TypeId ConstraintGraphBuilder::updateProperty(const ScopePtr& scope, AstExpr* ex
if (!lookupResult)
return check(scope, expr).ty;
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);
@ -2502,7 +2539,6 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
argTy = resolveType(signatureScope, local->annotation, /* inTypeArguments */ false, /* replaceErrorWithFresh*/ true);
else
{
if (i < expectedArgPack.head.size())
argTy = expectedArgPack.head[i];
else
@ -2511,7 +2547,15 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
argTypes.push_back(argTy);
argNames.emplace_back(FunctionArgument{local->name.value, 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);
signatureScope->lvalueTypes[bc->def] = argTy;
@ -3018,6 +3062,37 @@ void ConstraintGraphBuilder::prepopulateGlobalScope(const ScopePtr& globalScope,
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<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
// 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)
{
if (isBlocked(*destIter))
{
asMutable(*destIter)->ty.emplace<BoundType>(builtinTypes->errorRecoveryType());
asMutable(*destIter)->ty.emplace<BoundType>(builtinTypes->nilType);
unblock(*destIter, constraint->location);
}
@ -2013,57 +2013,26 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
if (get<FreeType>(iteratorTy))
return block_(iteratorTy);
auto anyify = [&](auto ty) {
Anyification anyify{arena, constraint->scope, builtinTypes, &iceReporter, builtinTypes->anyType, builtinTypes->anyTypePack};
std::optional anyified = anyify.substitute(ty);
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);
auto unpack = [&](TypeId ty) {
TypePackId variadic = arena->addTypePack(VariadicTypePack{ty});
pushConstraint(constraint->scope, constraint->location, UnpackConstraint{c.variables, variadic});
};
if (get<AnyType>(iteratorTy))
{
anyify(c.variables);
unpack(builtinTypes->anyType);
return true;
}
if (get<ErrorType>(iteratorTy))
{
errorify(c.variables);
unpack(builtinTypes->errorType);
return true;
}
if (get<NeverType>(iteratorTy))
{
neverify(c.variables);
unpack(builtinTypes->neverType);
return true;
}
@ -2088,7 +2057,7 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
unify(constraint->scope, constraint->location, c.variables, expectedVariablePack);
}
else
errorify(c.variables);
unpack(builtinTypes->errorType);
}
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});
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
{
@ -2154,9 +2123,9 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
LUAU_ASSERT(false);
}
else if (auto primitiveTy = get<PrimitiveType>(iteratorTy); primitiveTy && primitiveTy->type == PrimitiveType::Type::Table)
unknownify(c.variables);
unpack(builtinTypes->unknownType);
else
errorify(c.variables);
unpack(builtinTypes->errorType);
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
#include "Luau/DataFlowGraph.h"
#include "Luau/Ast.h"
#include "Luau/Breadcrumb.h"
#include "Luau/Error.h"
#include "Luau/Refinement.h"
#include <algorithm>
LUAU_FASTFLAG(DebugLuauFreezeArena)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
@ -278,11 +281,12 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f)
void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a)
{
for (AstExpr* r : a->values)
visitExpr(scope, r);
for (AstExpr* l : a->vars)
visitLValue(scope, l);
for (size_t i = 0; i < std::max(a->vars.size, a->values.size); ++i)
{
BreadcrumbId bc = i < a->values.size ? visitExpr(scope, a->values.data[i]) : breadcrumbs->add(nullptr, defs->freshCell());
if (i < a->vars.size)
visitLValue(scope, a->vars.data[i], bc);
}
}
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.
// 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]
// a += 5 -- a[2] = a[1] + 5
// local a = 5 -- a-1
// 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).
visitLValue(scope, c->var);
visitExpr(scope, c->value);
visitLValue(scope, c->var, visitExpr(scope, c->value));
}
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,
// but for bug compatibility, we'll assume the same thing here.
visitLValue(scope, f->name);
visitExpr(scope, f->func);
visitLValue(scope, f->name, visitExpr(scope, f->func));
}
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;
scope->bindings[l->name] = bc;
visitExpr(scope, l->func);
}
void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatTypeAlias* t)
@ -423,7 +423,7 @@ BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprLocal* l)
{
NullableBreadcrumbId breadcrumb = scope->lookup(l->local);
if (!breadcrumb)
handle->ice("AstExprLocal came before its declaration?");
handle->ice("DFG: AstExprLocal came before its declaration?");
graph.astBreadcrumbs[l] = breadcrumb;
return NotNull{breadcrumb};
@ -591,79 +591,67 @@ BreadcrumbId DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprError* erro
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>())
return visitLValue(scope, l);
return visitLValue(scope, l, bc);
else if (auto g = e->as<AstExprGlobal>())
return visitLValue(scope, g);
return visitLValue(scope, g, bc);
else if (auto i = e->as<AstExprIndexName>())
return visitLValue(scope, i);
return visitLValue(scope, i, bc);
else if (auto i = e->as<AstExprIndexExpr>())
return visitLValue(scope, i);
return visitLValue(scope, i, bc);
else if (auto error = e->as<AstExprError>())
{
visitExpr(scope, error); // TODO: is this right?
return;
}
return visitLValue(scope, error, bc);
else
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.
NullableBreadcrumbId bc = scope->lookup(l->local);
LUAU_ASSERT(bc);
graph.astBreadcrumbs[l] = bc;
scope->bindings[l->local] = bc;
// In order to avoid alias tracking, we need to clip the reference to the parent breadcrumb
// as well as the def that was about to be assigned onto this lvalue. However, we want to
// copy the metadata so that refinements can be consistent.
BreadcrumbId updated = breadcrumbs->add(scope->lookup(l->local), defs->freshCell(), bc->metadata);
graph.astBreadcrumbs[l] = updated;
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.
NullableBreadcrumbId bc = scope->lookup(g->name);
if (!bc)
bc = breadcrumbs->add(nullptr, defs->freshCell());
graph.astBreadcrumbs[g] = bc;
scope->bindings[g->name] = bc;
// In order to avoid alias tracking, we need to clip the reference to the parent breadcrumb
// as well as the def that was about to be assigned onto this lvalue. However, we want to
// copy the metadata so that refinements can be consistent.
BreadcrumbId updated = breadcrumbs->add(scope->lookup(g->name), defs->freshCell(), bc->metadata);
graph.astBreadcrumbs[g] = updated;
scope->bindings[g->name] = updated;
}
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);
std::string key = i->index.value;
NullableBreadcrumbId propBreadcrumb = scope->lookup(parentBreadcrumb->def, key);
if (!propBreadcrumb)
{
propBreadcrumb = breadcrumbs->emplace<FieldMetadata>(parentBreadcrumb, defs->freshCell(), key);
moduleScope->props[parentBreadcrumb->def][key] = propBreadcrumb;
BreadcrumbId updated = breadcrumbs->add(scope->props[parentBreadcrumb->def][i->index.value], defs->freshCell(), bc->metadata);
graph.astBreadcrumbs[i] = updated;
scope->props[parentBreadcrumb->def][i->index.value] = updated;
}
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);
visitExpr(scope, i->index);
if (auto string = i->index->as<AstExprConstantString>())
{
std::string key{string->value.data, string->value.size};
NullableBreadcrumbId propBreadcrumb = scope->lookup(parentBreadcrumb->def, key);
if (!propBreadcrumb)
{
propBreadcrumb = breadcrumbs->add(parentBreadcrumb, parentBreadcrumb->def);
moduleScope->props[parentBreadcrumb->def][key] = propBreadcrumb;
BreadcrumbId updated = breadcrumbs->add(scope->props[parentBreadcrumb->def][string->value.data], defs->freshCell(), bc->metadata);
graph.astBreadcrumbs[i] = updated;
scope->props[parentBreadcrumb->def][string->value.data] = updated;
}
}
graph.astBreadcrumbs[i] = propBreadcrumb;
}
void DataFlowGraphBuilder::visitLValue(DfgScope* scope, AstExprError* error, BreadcrumbId bc)
{
visitExpr(scope, error);
}
void DataFlowGraphBuilder::visitType(DfgScope* scope, AstType* t)

View file

@ -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 "
"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
@ -843,6 +850,12 @@ bool PackWhereClauseNeeded::operator==(const PackWhereClauseNeeded& rhs) const
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)
{
return toString(error, TypeErrorToStringOptions{});
@ -1009,6 +1022,11 @@ void copyError(T& e, TypeArena& destArena, CloneState& cloneState)
e.ty = clone(e.ty);
else if constexpr (std::is_same_v<T, PackWhereClauseNeeded>)
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
static_assert(always_false_v<T>, "Non-exhaustive type switch");
}

View file

@ -15,6 +15,7 @@
#include "Luau/StringUtils.h"
#include "Luau/TimeTrace.h"
#include "Luau/TypeChecker2.h"
#include "Luau/NonStrictTypeChecker.h"
#include "Luau/TypeInfer.h"
#include "Luau/Variant.h"
@ -37,7 +38,6 @@ LUAU_FASTFLAGVARIABLE(DebugLuauLogSolverToJson, false)
LUAU_FASTFLAGVARIABLE(DebugLuauReadWriteProperties, false)
LUAU_FASTFLAGVARIABLE(LuauTypecheckLimitControls, false)
LUAU_FASTFLAGVARIABLE(CorrectEarlyReturnInMarkDirty, false)
LUAU_FASTFLAGVARIABLE(DebugLuauNewNonStrictMode, false)
namespace Luau
{
@ -1212,17 +1212,17 @@ const SourceModule* Frontend::getSourceModule(const ModuleName& moduleName) cons
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,
const ScopePtr& parentScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options,
TypeCheckLimits limits)
{
const bool recordJsonLog = FFlag::DebugLuauLogSolverToJson;
return check(sourceModule, requireCycles, builtinTypes, iceHandler, moduleResolver, fileResolver, parentScope, std::move(prepareModuleScope),
options, limits, recordJsonLog);
return check(sourceModule, mode, requireCycles, builtinTypes, iceHandler, moduleResolver, fileResolver, parentScope,
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,
const ScopePtr& parentScope, std::function<void(const ModuleName&, const ScopePtr&)> prepareModuleScope, FrontendOptions options,
TypeCheckLimits limits, bool recordJsonLog)
@ -1303,6 +1303,9 @@ ModulePtr check(const SourceModule& sourceModule, const std::vector<RequireCycle
}
else
{
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());
}
@ -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,
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) {
if (prepareModuleScope)
@ -1341,7 +1344,7 @@ ModulePtr Frontend::check(const SourceModule& sourceModule, Mode mode, std::vect
try
{
return Luau::check(sourceModule, requireCycles, builtinTypes, NotNull{&iceHandler},
return Luau::check(sourceModule, mode, requireCycles, builtinTypes, NotNull{&iceHandler},
NotNull{forAutocomplete ? &moduleResolverForAutocomplete : &moduleResolver}, NotNull{fileResolver},
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) << " }";
else if constexpr (std::is_same_v<T, PackWhereClauseNeeded>)
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
static_assert(always_false_v<T>, "Non-exhaustive type switch");
}

View file

@ -14,6 +14,9 @@
LUAU_FASTINTVARIABLE(LuauSuggestionDistance, 4)
LUAU_FASTFLAGVARIABLE(LuauLintDeprecatedFenv, false)
LUAU_FASTFLAGVARIABLE(LuauLintTableIndexer, false)
namespace Luau
{
@ -2085,6 +2088,32 @@ private:
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)
{
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
{
AstExprIndexName* func = node->func->as<AstExprIndexName>();
if (!func)
return true;
if (AstExprGlobal* func = node->func->as<AstExprGlobal>())
{
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>();
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;
if (func->index == "insert" && node->args.size == 2)
@ -2245,8 +2308,6 @@ private:
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");
}
return true;
}
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
#include "Luau/NonStrictTypeChecker.h"
#include "Luau/Ast.h"
#include "Luau/Common.h"
#include "Luau/Type.h"
#include "Luau/Subtyping.h"
#include "Luau/Normalize.h"
#include "Luau/Error.h"
#include "Luau/TypeArena.h"
#include "Luau/TypeFamily.h"
#include "Luau/Def.h"
#include <iostream>
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
{
std::unordered_map<DefId, TypeId> context;
std::unordered_map<const Def*, TypeId> context;
NonStrictContext() = default;
@ -38,24 +79,14 @@ struct NonStrictContext
// 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 {};
}
// 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
@ -67,21 +98,341 @@ struct NonStrictTypeChecker
Module* module;
Normalizer normalizer;
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,
NotNull<UnifierSharedState> unifierState, Module* module)
NonStrictTypeChecker(NotNull<BuiltinTypes> builtinTypes, const NotNull<InternalErrorReporter> ice, NotNull<UnifierSharedState> unifierState,
NotNull<const DataFlowGraph> dfg, NotNull<TypeCheckLimits> limits, Module* module)
: builtinTypes(builtinTypes)
, ice(ice)
, module(module)
, normalizer{&arena, builtinTypes, unifierState, /* cache inhabitance */ true}
, 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
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

View file

@ -50,6 +50,8 @@ SubtypingResult& SubtypingResult::andAlso(const SubtypingResult& other)
// `|=` is intentional here, we want to preserve error related flags.
isErrorSuppressing |= other.isErrorSuppressing;
normalizationTooComplex |= other.normalizationTooComplex;
isCacheable &= other.isCacheable;
return *this;
}
@ -58,6 +60,8 @@ SubtypingResult& SubtypingResult::orElse(const SubtypingResult& other)
isSubtype |= other.isSubtype;
isErrorSuppressing |= other.isErrorSuppressing;
normalizationTooComplex |= other.normalizationTooComplex;
isCacheable &= other.isCacheable;
return *this;
}
@ -86,14 +90,23 @@ SubtypingResult SubtypingResult::any(const std::vector<SubtypingResult>& results
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)
{
mappedGenerics.clear();
mappedGenericPacks.clear();
SubtypingEnvironment env;
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& ub = bounds.upperBound;
@ -122,15 +135,42 @@ SubtypingResult Subtyping::isSubtype(TypeId subTy, TypeId superTy)
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;
}
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
@ -153,11 +193,19 @@ struct SeenSetPopper
};
} // namespace
SubtypingResult Subtyping::isCovariantWith(TypeId subTy, TypeId superTy)
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, TypeId subTy, TypeId superTy)
{
subTy = follow(subTy);
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?
// 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?
@ -167,7 +215,29 @@ SubtypingResult Subtyping::isCovariantWith(TypeId subTy, TypeId superTy)
std::pair<TypeId, TypeId> typePair{subTy, superTy};
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};
@ -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
// generics, so the upper bound is always unknown.
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))
return isCovariantWith(subTy, builtinTypes->unknownType);
return isCovariantWith(env, subTy, builtinTypes->unknownType);
SubtypingResult result;
if (auto subUnion = get<UnionType>(subTy))
return isCovariantWith(subUnion, superTy);
result = isCovariantWith(env, subUnion, superTy);
else if (auto superUnion = get<UnionType>(superTy))
return isCovariantWith(subTy, superUnion);
result = isCovariantWith(env, subTy, superUnion);
else if (auto superIntersection = get<IntersectionType>(superTy))
return isCovariantWith(subTy, superIntersection);
result = isCovariantWith(env, subTy, superIntersection);
else if (auto subIntersection = get<IntersectionType>(subTy))
{
SubtypingResult result = isCovariantWith(subIntersection, superTy);
if (result.isSubtype || result.isErrorSuppressing || result.normalizationTooComplex)
return result;
else
return isCovariantWith(normalizer->normalize(subTy), normalizer->normalize(superTy));
result = isCovariantWith(env, subIntersection, superTy);
if (!result.isSubtype && !result.isErrorSuppressing && !result.normalizationTooComplex)
result = isCovariantWith(env, normalizer->normalize(subTy), normalizer->normalize(superTy));
}
else if (get<AnyType>(superTy))
return {true}; // This is always true.
result = {true};
else if (get<AnyType>(subTy))
{
// any = unknown | error, so we rewrite this to match.
// 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))
{
@ -208,57 +278,59 @@ SubtypingResult Subtyping::isCovariantWith(TypeId subTy, TypeId superTy)
LUAU_ASSERT(!get<IntersectionType>(subTy)); // TODO: replace with ice.
bool errorSuppressing = get<ErrorType>(subTy);
return {!errorSuppressing, errorSuppressing};
result = {!errorSuppressing, errorSuppressing};
}
else if (get<NeverType>(subTy))
return {true};
result = {true};
else if (get<ErrorType>(superTy))
return {false, true};
result = {false, true};
else if (get<ErrorType>(subTy))
return {false, true};
result = {false, true};
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))
return isCovariantWith(subNegation, superTy);
result = isCovariantWith(env, subNegation, 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)
{
bool ok = bindGeneric(subTy, superTy);
return {ok};
bool ok = bindGeneric(env, subTy, superTy);
result.isSubtype = ok;
result.isCacheable = false;
}
else if (auto superGeneric = get<GenericType>(superTy); superGeneric && variance == Variance::Contravariant)
{
bool ok = bindGeneric(subTy, superTy);
return {ok};
bool ok = bindGeneric(env, subTy, superTy);
result.isSubtype = ok;
result.isCacheable = false;
}
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))
return isCovariantWith(p);
result = isCovariantWith(env, p);
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))
return isCovariantWith(p);
result = isCovariantWith(env, p);
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))
return isCovariantWith(p);
result = isCovariantWith(env, p);
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))
return isCovariantWith(p);
result = isCovariantWith(env, p);
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))
return isCovariantWith(p);
result = isCovariantWith(env, p);
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);
superTp = follow(superTp);
@ -278,7 +350,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
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)
return {false};
}
@ -292,7 +364,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
if (auto vt = get<VariadicTypePack>(*subTail))
{
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))
{
@ -306,10 +378,10 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
std::vector<TypeId> headSlice(begin(superHead), begin(superHead) + headSize);
TypePackId superTailPack = arena->addTypePack(std::move(headSlice), superTail);
if (TypePackId* other = mappedGenericPacks.find(*subTail))
results.push_back(isCovariantWith(*other, superTailPack));
if (TypePackId* other = env.mappedGenericPacks.find(*subTail))
results.push_back(isCovariantWith(env, *other, superTailPack));
else
mappedGenericPacks.try_insert(*subTail, superTailPack);
env.mappedGenericPacks.try_insert(*subTail, superTailPack);
// FIXME? Not a fan of the early return here. It makes the
// control flow harder to reason about.
@ -337,7 +409,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
if (auto vt = get<VariadicTypePack>(*superTail))
{
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))
{
@ -351,10 +423,10 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
std::vector<TypeId> headSlice(begin(subHead), begin(subHead) + headSize);
TypePackId subTailPack = arena->addTypePack(std::move(headSlice), subTail);
if (TypePackId* other = mappedGenericPacks.find(*superTail))
results.push_back(isCovariantWith(*other, subTailPack));
if (TypePackId* other = env.mappedGenericPacks.find(*superTail))
results.push_back(isCovariantWith(env, *other, subTailPack));
else
mappedGenericPacks.try_insert(*superTail, subTailPack);
env.mappedGenericPacks.try_insert(*superTail, subTailPack);
// FIXME? Not a fan of the early return here. It makes the
// 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))
{
results.push_back(isCovariantWith(p));
results.push_back(isCovariantWith(env, p));
}
else if (auto p = get2<GenericTypePack, GenericTypePack>(*subTail, *superTail))
{
bool ok = bindGeneric(*subTail, *superTail);
bool ok = bindGeneric(env, *subTail, *superTail);
results.push_back({ok});
}
else if (get2<VariadicTypePack, GenericTypePack>(*subTail, *superTail))
@ -393,7 +465,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
if (variance == Variance::Contravariant)
{
// <A...>(A...) -> number <: (...number) -> number
bool ok = bindGeneric(*subTail, *superTail);
bool ok = bindGeneric(env, *subTail, *superTail);
results.push_back({ok});
}
else
@ -412,7 +484,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
else
{
// <A...>() -> A... <: () -> ...number
bool ok = bindGeneric(*subTail, *superTail);
bool ok = bindGeneric(env, *subTail, *superTail);
results.push_back({ok});
}
}
@ -428,7 +500,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
}
else if (get<GenericTypePack>(*subTail))
{
bool ok = bindGeneric(*subTail, builtinTypes->emptyTypePack);
bool ok = bindGeneric(env, *subTail, builtinTypes->emptyTypePack);
return {ok};
}
else
@ -452,7 +524,7 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
{
if (variance == Variance::Contravariant)
{
bool ok = bindGeneric(builtinTypes->emptyTypePack, *superTail);
bool ok = bindGeneric(env, builtinTypes->emptyTypePack, *superTail);
results.push_back({ok});
}
else
@ -466,33 +538,33 @@ SubtypingResult Subtyping::isCovariantWith(TypePackId subTp, TypePackId superTp)
}
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>
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>
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>
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>
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
* 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
std::vector<SubtypingResult> subtypings;
for (TypeId ty : superUnion)
subtypings.push_back(isCovariantWith(subTy, ty));
subtypings.push_back(isCovariantWith(env, subTy, ty));
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
std::vector<SubtypingResult> subtypings;
for (TypeId ty : subUnion)
subtypings.push_back(isCovariantWith(ty, superTy));
subtypings.push_back(isCovariantWith(env, ty, superTy));
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
std::vector<SubtypingResult> subtypings;
for (TypeId ty : superIntersection)
subtypings.push_back(isCovariantWith(subTy, ty));
subtypings.push_back(isCovariantWith(env, subTy, ty));
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
std::vector<SubtypingResult> subtypings;
for (TypeId ty : subIntersection)
subtypings.push_back(isCovariantWith(ty, superTy));
subtypings.push_back(isCovariantWith(env, ty, superTy));
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);
@ -572,17 +644,17 @@ SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, Type
if (is<NeverType>(negatedTy))
{
// ¬never ~ unknown
return isCovariantWith(builtinTypes->unknownType, superTy);
return isCovariantWith(env, builtinTypes->unknownType, superTy);
}
else if (is<UnknownType>(negatedTy))
{
// ¬unknown ~ never
return isCovariantWith(builtinTypes->neverType, superTy);
return isCovariantWith(env, builtinTypes->neverType, superTy);
}
else if (is<AnyType>(negatedTy))
{
// ¬any ~ any
return isCovariantWith(negatedTy, superTy);
return isCovariantWith(env, negatedTy, superTy);
}
else if (auto u = get<UnionType>(negatedTy))
{
@ -593,7 +665,7 @@ SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, Type
for (TypeId ty : u)
{
NegationType negatedTmp{ty};
subtypings.push_back(isCovariantWith(&negatedTmp, superTy));
subtypings.push_back(isCovariantWith(env, &negatedTmp, superTy));
}
return SubtypingResult::all(subtypings);
@ -607,11 +679,11 @@ SubtypingResult Subtyping::isCovariantWith(const NegationType* subNegation, Type
for (TypeId ty : i)
{
if (auto negatedPart = get<NegationType>(follow(ty)))
subtypings.push_back(isCovariantWith(negatedPart->ty, superTy));
subtypings.push_back(isCovariantWith(env, negatedPart->ty, superTy));
else
{
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);
if (is<NeverType>(negatedTy))
{
// ¬never ~ unknown
return isCovariantWith(subTy, builtinTypes->unknownType);
return isCovariantWith(env, subTy, builtinTypes->unknownType);
}
else if (is<UnknownType>(negatedTy))
{
// ¬unknown ~ never
return isCovariantWith(subTy, builtinTypes->neverType);
return isCovariantWith(env, subTy, builtinTypes->neverType);
}
else if (is<AnyType>(negatedTy))
{
@ -657,11 +729,11 @@ SubtypingResult Subtyping::isCovariantWith(const TypeId subTy, const NegationTyp
for (TypeId ty : u)
{
if (auto negatedPart = get<NegationType>(follow(ty)))
subtypings.push_back(isCovariantWith(subTy, negatedPart->ty));
subtypings.push_back(isCovariantWith(env, subTy, negatedPart->ty));
else
{
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)
{
if (auto negatedPart = get<NegationType>(follow(ty)))
subtypings.push_back(isCovariantWith(subTy, negatedPart->ty));
subtypings.push_back(isCovariantWith(env, subTy, negatedPart->ty));
else
{
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))
return {*p.first != *p.second};
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))
return {true};
else if (is<ErrorType, FunctionType, TableType, MetatableType>(negatedTy))
@ -733,12 +805,12 @@ SubtypingResult Subtyping::isCovariantWith(const TypeId subTy, const NegationTyp
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};
}
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)
return {true};
@ -748,12 +820,12 @@ SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, co
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};
}
SubtypingResult Subtyping::isCovariantWith(const TableType* subTable, const TableType* superTable)
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const TableType* subTable, const TableType* superTable)
{
SubtypingResult result{true};
@ -764,12 +836,12 @@ SubtypingResult Subtyping::isCovariantWith(const TableType* subTable, const Tabl
{
std::vector<SubtypingResult> results;
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 (isInvariantWith(subTable->indexer->indexType, builtinTypes->stringType).isSubtype)
results.push_back(isInvariantWith(subTable->indexer->indexResultType, prop.type()));
if (isInvariantWith(env, subTable->indexer->indexType, builtinTypes->stringType).isSubtype)
results.push_back(isInvariantWith(env, subTable->indexer->indexResultType, prop.type()));
}
if (results.empty())
@ -781,7 +853,7 @@ SubtypingResult Subtyping::isCovariantWith(const TableType* subTable, const Tabl
if (superTable->indexer)
{
if (subTable->indexer)
result.andAlso(isInvariantWith(*subTable->indexer, *superTable->indexer));
result.andAlso(isInvariantWith(env, *subTable->indexer, *superTable->indexer));
else
return {false};
}
@ -789,12 +861,12 @@ SubtypingResult Subtyping::isCovariantWith(const TableType* subTable, const Tabl
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))
{
@ -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
// compatible properties/shapes. We'll revisit this later when we have a
// better understanding of how important this is.
return isCovariantWith(subTable, superTable);
return isCovariantWith(env, subTable, superTable);
}
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)};
}
SubtypingResult Subtyping::isCovariantWith(const ClassType* subClass, const TableType* superTable)
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const ClassType* subClass, const TableType* superTable)
{
SubtypingResult result{true};
for (const auto& [name, prop] : superTable->props)
{
if (auto classProp = lookupClassProp(subClass, name))
result.andAlso(isInvariantWith(prop.type(), classProp->type()));
result.andAlso(isInvariantWith(env, prop.type(), classProp->type()));
else
return SubtypingResult{false};
}
@ -836,20 +908,20 @@ SubtypingResult Subtyping::isCovariantWith(const ClassType* subClass, const Tabl
return result;
}
SubtypingResult Subtyping::isCovariantWith(const FunctionType* subFunction, const FunctionType* superFunction)
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const FunctionType* subFunction, const FunctionType* superFunction)
{
SubtypingResult result;
{
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;
}
SubtypingResult Subtyping::isCovariantWith(const PrimitiveType* subPrim, const TableType* superTable)
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const PrimitiveType* subPrim, const TableType* superTable)
{
SubtypingResult result{false};
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 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;
}
SubtypingResult Subtyping::isCovariantWith(const SingletonType* subSingleton, const TableType* superTable)
SubtypingResult Subtyping::isCovariantWith(SubtypingEnvironment& env, const SingletonType* subSingleton, const TableType* superTable)
{
SubtypingResult result{false};
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 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;
}
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)
return {false, true, true};
SubtypingResult result = isCovariantWith(subNorm->tops, superNorm->tops);
result.andAlso(isCovariantWith(subNorm->booleans, superNorm->booleans));
result.andAlso(isCovariantWith(subNorm->classes, superNorm->classes).orElse(isCovariantWith(subNorm->classes, superNorm->tables)));
result.andAlso(isCovariantWith(subNorm->errors, superNorm->errors));
result.andAlso(isCovariantWith(subNorm->nils, superNorm->nils));
result.andAlso(isCovariantWith(subNorm->numbers, superNorm->numbers));
result.andAlso(isCovariantWith(subNorm->strings, superNorm->strings));
result.andAlso(isCovariantWith(subNorm->strings, superNorm->tables));
result.andAlso(isCovariantWith(subNorm->threads, superNorm->threads));
result.andAlso(isCovariantWith(subNorm->tables, superNorm->tables));
result.andAlso(isCovariantWith(subNorm->functions, superNorm->functions));
SubtypingResult result = isCovariantWith(env, subNorm->tops, superNorm->tops);
result.andAlso(isCovariantWith(env, subNorm->booleans, superNorm->booleans));
result.andAlso(isCovariantWith(env, subNorm->classes, superNorm->classes).orElse(isCovariantWith(env, subNorm->classes, superNorm->tables)));
result.andAlso(isCovariantWith(env, subNorm->errors, superNorm->errors));
result.andAlso(isCovariantWith(env, subNorm->nils, superNorm->nils));
result.andAlso(isCovariantWith(env, subNorm->numbers, superNorm->numbers));
result.andAlso(isCovariantWith(env, subNorm->strings, superNorm->strings));
result.andAlso(isCovariantWith(env, subNorm->strings, superNorm->tables));
result.andAlso(isCovariantWith(env, subNorm->threads, superNorm->threads));
result.andAlso(isCovariantWith(env, subNorm->tables, superNorm->tables));
result.andAlso(isCovariantWith(env, subNorm->functions, superNorm->functions));
// isCovariantWith(subNorm->tyvars, superNorm->tyvars);
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)
{
@ -923,13 +996,13 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass,
for (const auto& [superClassTy, superNegations] : superClass.classes)
{
result.orElse(isCovariantWith(subClassTy, superClassTy));
result.orElse(isCovariantWith(env, subClassTy, superClassTy));
if (!result.isSubtype)
continue;
for (TypeId negation : superNegations)
{
result.andAlso(SubtypingResult::negate(isCovariantWith(subClassTy, negation)));
result.andAlso(SubtypingResult::negate(isCovariantWith(env, subClassTy, negation)));
if (result.isSubtype)
break;
}
@ -942,14 +1015,14 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass,
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)
{
SubtypingResult result;
for (TypeId superTableTy : superTables)
result.orElse(isCovariantWith(subClassTy, superTableTy));
result.orElse(isCovariantWith(env, subClassTy, superTableTy));
if (!result.isSubtype)
return result;
@ -958,13 +1031,13 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedClassType& subClass,
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);
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())
return {true};
@ -974,7 +1047,7 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString
SubtypingResult result;
for (const auto& superTable : superTables)
{
result.orElse(isCovariantWith(builtinTypes->stringType, superTable));
result.orElse(isCovariantWith(env, builtinTypes->stringType, superTable));
if (result.isSubtype)
return result;
}
@ -990,7 +1063,7 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString
SubtypingResult result{true};
for (const auto& [_, subString] : subString.singletons)
{
result.andAlso(isCovariantWith(subString, superTable));
result.andAlso(isCovariantWith(env, subString, superTable));
if (!result.isSubtype)
break;
}
@ -1004,17 +1077,18 @@ SubtypingResult Subtyping::isCovariantWith(const NormalizedStringType& subString
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())
return {true};
else if (superFunction.isTop)
return {true};
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;
@ -1022,32 +1096,32 @@ SubtypingResult Subtyping::isCovariantWith(const TypeIds& subTypes, const TypeId
{
results.emplace_back();
for (TypeId superTy : superTypes)
results.back().orElse(isCovariantWith(subTy, superTy));
results.back().orElse(isCovariantWith(env, subTy, superTy));
}
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 (!get<GenericType>(subTy))
return false;
mappedGenerics[subTy].upperBound.insert(superTy);
env.mappedGenerics[subTy].upperBound.insert(superTy);
}
else
{
if (!get<GenericType>(superTy))
return false;
mappedGenerics[superTy].lowerBound.insert(subTy);
env.mappedGenerics[superTy].lowerBound.insert(subTy);
}
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
* type.
*/
bool Subtyping::bindGeneric(TypePackId subTp, TypePackId superTp)
bool Subtyping::bindGeneric(SubtypingEnvironment& env, TypePackId subTp, TypePackId superTp)
{
if (variance == Variance::Contravariant)
std::swap(superTp, subTp);
@ -1066,10 +1140,10 @@ bool Subtyping::bindGeneric(TypePackId subTp, TypePackId superTp)
if (!get<GenericTypePack>(subTp))
return false;
if (TypePackId* m = mappedGenericPacks.find(subTp))
if (TypePackId* m = env.mappedGenericPacks.find(subTp))
return *m == superTp;
mappedGenericPacks[subTp] = superTp;
env.mappedGenericPacks[subTp] = superTp;
return true;
}

View file

@ -8,6 +8,7 @@
#include "Luau/Normalize.h"
#include "Luau/Simplify.h"
#include "Luau/Substitution.h"
#include "Luau/Subtyping.h"
#include "Luau/ToString.h"
#include "Luau/TxnLog.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));
}
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(
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())
{
@ -333,34 +351,28 @@ TypeFamilyReductionResult<TypeId> numericBinopFamilyFn(
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, {}, {}};
}
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, {}, {}};
}
else if ((normLhsTy->hasNumbers() || normLhsTy->hasTops()) && (normRhsTy->hasNumbers() || normRhsTy->hasTops()))
{
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))
{
// 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, {}, {}};
}
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}, {}};
}
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.
@ -385,10 +397,14 @@ TypeFamilyReductionResult<TypeId> numericBinopFamilyFn(
if (!mmFtv)
return {std::nullopt, true, {}, {}};
if (std::optional<TypeId> instantiatedMmType = instantiate(ctx->builtins, ctx->arena, ctx->limits, ctx->scope, *mmType))
{
if (const FunctionType* instantiatedMmFtv = get<FunctionType>(*instantiatedMmType))
{
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};
@ -405,19 +421,8 @@ TypeFamilyReductionResult<TypeId> numericBinopFamilyFn(
else
return {std::nullopt, true, {}, {}};
}
else
{
return {ctx->builtins->errorRecoveryType(), false, {}, {}};
}
}
else
{
// TODO: Not the nicest logic here.
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())
{
@ -428,7 +433,7 @@ TypeFamilyReductionResult<TypeId> addFamilyFn(std::vector<TypeId> typeParams, st
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())
{
@ -439,7 +444,7 @@ TypeFamilyReductionResult<TypeId> subFamilyFn(std::vector<TypeId> typeParams, st
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())
{
@ -450,7 +455,7 @@ TypeFamilyReductionResult<TypeId> mulFamilyFn(std::vector<TypeId> typeParams, st
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())
{
@ -461,7 +466,7 @@ TypeFamilyReductionResult<TypeId> divFamilyFn(std::vector<TypeId> typeParams, st
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())
{
@ -472,7 +477,7 @@ TypeFamilyReductionResult<TypeId> idivFamilyFn(std::vector<TypeId> typeParams, s
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())
{
@ -483,7 +488,7 @@ TypeFamilyReductionResult<TypeId> powFamilyFn(std::vector<TypeId> typeParams, st
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())
{
@ -494,7 +499,91 @@ TypeFamilyReductionResult<TypeId> modFamilyFn(std::vector<TypeId> typeParams, st
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())
{
@ -522,7 +611,7 @@ TypeFamilyReductionResult<TypeId> andFamilyFn(std::vector<TypeId> typeParams, st
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())
{
@ -550,16 +639,196 @@ TypeFamilyReductionResult<TypeId> orFamilyFn(std::vector<TypeId> typeParams, std
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()
: addFamily{"Add", addFamilyFn}
, subFamily{"Sub", subFamilyFn}
, mulFamily{"Mul", mulFamilyFn}
, divFamily{"Div", divFamilyFn}
, idivFamily{"FloorDiv", idivFamilyFn}
, powFamily{"Exp", powFamilyFn}
, modFamily{"Mod", modFamilyFn}
, andFamily{"And", andFamilyFn}
, orFamily{"Or", orFamilyFn}
: notFamily{"not", notFamilyFn}
, addFamily{"add", addFamilyFn}
, subFamily{"sub", subFamilyFn}
, mulFamily{"mul", mulFamilyFn}
, divFamily{"div", divFamilyFn}
, idivFamily{"idiv", idivFamilyFn}
, powFamily{"pow", powFamilyFn}
, modFamily{"mod", modFamilyFn}
, 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[powFamily.name] = mkBinaryTypeFamily(&powFamily);
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

View file

@ -12,7 +12,6 @@
#include "Luau/TypeUtils.h"
#include "Luau/Type.h"
#include "Luau/VisitType.h"
#include "Luau/TypeFamily.h"
#include <algorithm>
@ -453,32 +452,10 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
blockedTypes.push_back(superTy);
if (log.get<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);
}
ice("Unexpected TypeFamilyInstanceType superTy");
if (log.get<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;
}
ice("Unexpected TypeFamilyInstanceType subTy");
auto superFree = log.getMutable<FreeType>(superTy);
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,
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,
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;
@ -393,7 +393,8 @@ public:
AstName debugname;
bool hasEnd = false;
// TODO clip with FFlag::LuauClipExtraHasEndProps
bool DEPRECATED_hasEnd = false;
std::optional<Location> argLocation;
};
@ -539,6 +540,17 @@ public:
void visit(AstVisitor* visitor) override;
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;
};
@ -548,7 +560,7 @@ public:
LUAU_RTTI(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>& elseLocation, bool DEPRECATED_hasEnd);
void visit(AstVisitor* visitor) override;
@ -561,7 +573,8 @@ public:
// Active for 'elseif' as well
std::optional<Location> elseLocation;
bool hasEnd = false;
// TODO clip with FFlag::LuauClipExtraHasEndProps
bool DEPRECATED_hasEnd = false;
};
class AstStatWhile : public AstStat
@ -569,7 +582,7 @@ class AstStatWhile : public AstStat
public:
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;
@ -579,7 +592,8 @@ public:
bool hasDo = false;
Location doLocation;
bool hasEnd = false;
// TODO clip with FFlag::LuauClipExtraHasEndProps
bool DEPRECATED_hasEnd = false;
};
class AstStatRepeat : public AstStat
@ -587,14 +601,14 @@ class AstStatRepeat : public AstStat
public:
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;
AstExpr* condition;
AstStatBlock* body;
bool hasUntil = false;
bool DEPRECATED_hasUntil = false;
};
class AstStatBreak : public AstStat
@ -663,7 +677,7 @@ public:
LUAU_RTTI(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);
void visit(AstVisitor* visitor) override;
@ -676,7 +690,8 @@ public:
bool hasDo = false;
Location doLocation;
bool hasEnd = false;
// TODO clip with FFlag::LuauClipExtraHasEndProps
bool DEPRECATED_hasEnd = false;
};
class AstStatForIn : public AstStat
@ -685,7 +700,7 @@ public:
LUAU_RTTI(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);
const Location& inLocation, bool hasDo, const Location& doLocation, bool DEPRECATED_hasEnd);
void visit(AstVisitor* visitor) override;
@ -699,7 +714,8 @@ public:
bool hasDo = false;
Location doLocation;
bool hasEnd = false;
// TODO clip with FFlag::LuauClipExtraHasEndProps
bool DEPRECATED_hasEnd = false;
};
class AstStatAssign : public AstStat

View file

@ -3,7 +3,7 @@
#include "Luau/Common.h"
LUAU_FASTFLAG(LuauFloorDivision)
LUAU_FASTFLAG(LuauFloorDivision);
namespace Luau
{
@ -164,7 +164,7 @@ void AstExprIndexExpr::visit(AstVisitor* visitor)
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,
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)
: AstExpr(ClassIndex(), location)
, generics(generics)
@ -178,7 +178,7 @@ AstExprFunction::AstExprFunction(const Location& location, const AstArray<AstGen
, body(body)
, functionDepth(functionDepth)
, debugname(debugname)
, hasEnd(hasEnd)
, DEPRECATED_hasEnd(DEPRECATED_hasEnd)
, argLocation(argLocation)
{
}
@ -397,14 +397,14 @@ void AstStatBlock::visit(AstVisitor* visitor)
}
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)
, condition(condition)
, thenbody(thenbody)
, elsebody(elsebody)
, thenLocation(thenLocation)
, 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)
, condition(condition)
, body(body)
, hasDo(hasDo)
, 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)
, condition(condition)
, 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,
const Location& doLocation, bool hasEnd)
const Location& doLocation, bool DEPRECATED_hasEnd)
: AstStat(ClassIndex(), location)
, var(var)
, from(from)
@ -537,7 +537,7 @@ AstStatFor::AstStatFor(const Location& location, AstLocal* var, AstExpr* from, A
, body(body)
, hasDo(hasDo)
, 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,
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)
, vars(vars)
, values(values)
@ -568,7 +568,7 @@ AstStatForIn::AstStatForIn(const Location& location, const AstArray<AstLocal*>&
, inLocation(inLocation)
, hasDo(hasDo)
, doLocation(doLocation)
, hasEnd(hasEnd)
, DEPRECATED_hasEnd(DEPRECATED_hasEnd)
{
}

View file

@ -15,6 +15,7 @@
LUAU_FASTINTVARIABLE(LuauRecursionLimit, 1000)
LUAU_FASTINTVARIABLE(LuauParseErrorLimit, 100)
LUAU_FASTFLAGVARIABLE(LuauParseDeclareClassIndexer, false)
LUAU_FASTFLAGVARIABLE(LuauClipExtraHasEndProps, false)
LUAU_FASTFLAG(LuauFloorDivision)
LUAU_FASTFLAG(LuauCheckedFunctionSyntax)
@ -371,16 +372,18 @@ AstStat* Parser::parseIf()
AstStat* elsebody = nullptr;
Location end = start;
std::optional<Location> elseLocation;
bool hasEnd = false;
bool DEPRECATED_hasEnd = false;
if (lexer.current().type == Lexeme::ReservedElseif)
{
if (FFlag::LuauClipExtraHasEndProps)
thenbody->hasEnd = true;
unsigned int recursionCounterOld = recursionCounter;
incrementRecursionCounter("elseif");
elseLocation = lexer.current().location;
elsebody = parseIf();
end = elsebody->location;
hasEnd = elsebody->as<AstStatIf>()->hasEnd;
DEPRECATED_hasEnd = elsebody->as<AstStatIf>()->DEPRECATED_hasEnd;
recursionCounter = recursionCounterOld;
}
else
@ -389,6 +392,8 @@ AstStat* Parser::parseIf()
if (lexer.current().type == Lexeme::ReservedElse)
{
if (FFlag::LuauClipExtraHasEndProps)
thenbody->hasEnd = true;
elseLocation = lexer.current().location;
matchThenElse = lexer.current();
nextLexeme();
@ -399,10 +404,22 @@ AstStat* Parser::parseIf()
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
@ -426,6 +443,8 @@ AstStat* Parser::parseWhile()
Location end = lexer.current().location;
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);
}
@ -447,6 +466,8 @@ AstStat* Parser::parseRepeat()
functionStack.back().loopDepth--;
bool hasUntil = expectMatchEndAndConsume(Lexeme::ReservedUntil, matchRepeat);
if (FFlag::LuauClipExtraHasEndProps)
body->hasEnd = hasUntil;
AstExpr* cond = parseExpr();
@ -543,6 +564,8 @@ AstStat* Parser::parseFor()
Location end = lexer.current().location;
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);
}
@ -585,6 +608,8 @@ AstStat* Parser::parseFor()
Location end = lexer.current().location;
bool hasEnd = expectMatchEndAndConsume(Lexeme::ReservedEnd, matchDo);
if (FFlag::LuauClipExtraHasEndProps)
body->hasEnd = hasEnd;
return allocator.alloc<AstStatForIn>(
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;
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,
functionStack.size(), debugname, typelist, varargAnnotation, hasEnd, argLocation),

View file

@ -20,7 +20,7 @@ struct IrFunction;
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);

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
// '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 Luau

View file

@ -48,10 +48,9 @@ inline void gatherFunctions(std::vector<Proto*>& results, Proto* proto, unsigned
}
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?
std::vector<uint32_t> bcLocations(function.instructions.size() + 1, ~0u);
@ -202,22 +201,22 @@ inline bool lowerImpl(AssemblyBuilder& build, IrLowering& lowering, IrFunction&
return true;
}
inline bool lowerIr(
X64::AssemblyBuilderX64& build, IrBuilder& ir, ModuleHelpers& helpers, Proto* proto, AssemblyOptions options, LoweringStats* stats)
inline bool lowerIr(X64::AssemblyBuilderX64& build, IrBuilder& ir, const std::vector<uint32_t>& sortedBlocks, ModuleHelpers& helpers, Proto* proto,
AssemblyOptions options, LoweringStats* stats)
{
optimizeMemoryOperandsX64(ir.function);
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(
A64::AssemblyBuilderA64& build, IrBuilder& ir, ModuleHelpers& helpers, Proto* proto, AssemblyOptions options, LoweringStats* stats)
inline bool lowerIr(A64::AssemblyBuilderA64& build, IrBuilder& ir, const std::vector<uint32_t>& sortedBlocks, ModuleHelpers& helpers, Proto* proto,
AssemblyOptions options, LoweringStats* 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>
@ -237,7 +236,12 @@ inline bool lowerFunction(IrBuilder& ir, AssemblyBuilder& build, ModuleHelpers&
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

View file

@ -54,15 +54,30 @@ void updateUseCounts(IrFunction& function)
}
}
void updateLastUseLocations(IrFunction& function)
void updateLastUseLocations(IrFunction& function, const std::vector<uint32_t>& sortedBlocks)
{
std::vector<IrInst>& instructions = function.instructions;
#if defined(LUAU_ASSERTENABLED)
// Last use assignements should be called only once
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)
{
uint32_t blockIndex = sortedBlocks[i];
IrBlock& block = function.blocks[blockIndex];
if (block.kind == IrBlockKind::Dead)
continue;
LUAU_ASSERT(block.start != ~0u);
LUAU_ASSERT(block.finish != ~0u);
for (uint32_t instIdx = block.start; instIdx <= block.finish; instIdx++)
{
LUAU_ASSERT(instIdx < function.instructions.size());
IrInst& inst = instructions[instIdx];
auto checkOp = [&](IrOp op) {
@ -81,6 +96,7 @@ void updateLastUseLocations(IrFunction& function)
checkOp(inst.f);
}
}
}
uint32_t getNextInstUse(IrFunction& function, uint32_t targetInstIdx, uint32_t startInstIdx)
{

View file

@ -249,9 +249,6 @@ IrLoweringA64::IrLoweringA64(AssemblyBuilderA64& build, ModuleHelpers& helpers,
, valueTracker(function)
, 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) {
IrLoweringA64* self = static_cast<IrLoweringA64*>(context);
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
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)
return mem(rBase, vmRegOp(op) * sizeof(TValue) + offset);

View file

@ -31,9 +31,6 @@ IrLoweringX64::IrLoweringX64(AssemblyBuilderX64& build, ModuleHelpers& helpers,
, valueTracker(function)
, 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) {
((IrRegAllocX64*)context)->restore(inst, false);
});

View file

@ -886,7 +886,7 @@ std::vector<uint32_t> getSortedBlockOrder(IrFunction& function)
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)
{

View file

@ -30,7 +30,7 @@ LUAU_FASTFLAGVARIABLE(LuauCompileFenvNoBuiltinFold, false)
LUAU_FASTFLAGVARIABLE(LuauCompileTopCold, false)
LUAU_FASTFLAG(LuauFloorDivision)
LUAU_FASTFLAGVARIABLE(LuauCompileFixContinueValidation, false)
LUAU_FASTFLAGVARIABLE(LuauCompileFixContinueValidation2, false)
LUAU_FASTFLAGVARIABLE(LuauCompileContinueCloseUpvals, false)
@ -276,7 +276,7 @@ struct Compiler
f.upvals = upvals;
// 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.stackSize = stackSize;
@ -665,14 +665,6 @@ struct Compiler
else
{
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,24 +713,9 @@ struct Compiler
{
AstLocal* local = func->args.data[i];
if (FFlag::LuauCompileFixContinueValidation)
{
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);
}
@ -2510,12 +2487,18 @@ struct Compiler
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
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);
// fallthrough = proceed with the loop body as usual
@ -2577,7 +2560,7 @@ struct Compiler
size_t oldJumps = loopJumps.size();
size_t oldLocals = localStack.size();
loops.push_back({oldLocals, oldLocals, nullptr});
loops.push_back({oldLocals, oldLocals, nullptr, nullptr});
hasLoops = true;
size_t loopLabel = bytecode.emitLabel();
@ -2613,7 +2596,7 @@ struct Compiler
size_t oldJumps = loopJumps.size();
size_t oldLocals = localStack.size();
loops.push_back({oldLocals, oldLocals, stat->condition});
loops.push_back({oldLocals, oldLocals, stat->condition, nullptr});
hasLoops = true;
size_t loopLabel = bytecode.emitLabel();
@ -2624,6 +2607,8 @@ struct Compiler
RegScope rs(this);
bool continueValidated = false;
for (size_t i = 0; i < body->body.size; ++i)
{
compileStat(body->body.data[i]);
@ -2634,6 +2619,14 @@ struct Compiler
// expression that continue will jump to.
if (FFlag::LuauCompileContinueCloseUpvals)
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();
@ -2762,19 +2755,7 @@ struct Compiler
{
// 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 (FFlag::LuauCompileFixContinueValidation)
{
// Mark that optimization skipped allocation of this local
for (AstLocal* local : stat->vars)
{
Local& l = locals[local];
l.skipped = true;
}
}
return;
}
// 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)
@ -2863,7 +2844,7 @@ struct Compiler
size_t oldLocals = localStack.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)
{
@ -2914,7 +2895,7 @@ struct Compiler
size_t oldLocals = localStack.size();
size_t oldJumps = loopJumps.size();
loops.push_back({oldLocals, oldLocals, nullptr});
loops.push_back({oldLocals, oldLocals, nullptr, nullptr});
hasLoops = true;
// register layout: limit, step, index
@ -2979,7 +2960,7 @@ struct Compiler
size_t oldLocals = localStack.size();
size_t oldJumps = loopJumps.size();
loops.push_back({oldLocals, oldLocals, nullptr});
loops.push_back({oldLocals, oldLocals, nullptr, nullptr});
hasLoops = true;
// register layout: generator, state, index, variables...
@ -3391,7 +3372,13 @@ struct Compiler
{
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);
// 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)
{
LUAU_ASSERT(!FFlag::LuauCompileFixContinueValidation2);
UndefinedLocalVisitor visitor(this);
condition->visit(&visitor);
@ -3486,6 +3474,32 @@ struct Compiler
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)
{
ConstUpvalueVisitor visitor(this);
@ -3702,19 +3716,25 @@ struct Compiler
UndefinedLocalVisitor(Compiler* self)
: self(self)
, undef(nullptr)
, locals(nullptr)
{
}
void check(AstLocal* local)
{
if (FFlag::LuauCompileFixContinueValidation2)
{
if (!undef && locals.contains(local))
undef = local;
}
else
{
Local& l = self->locals[local];
if (FFlag::LuauCompileFixContinueValidation && l.skipped)
return;
if (!l.allocated && !undef)
undef = local;
}
}
bool visit(AstExprLocal* node) override
{
@ -3742,6 +3762,7 @@ struct Compiler
Compiler* self;
AstLocal* undef;
DenseHashSet<AstLocal*> locals;
};
struct ConstUpvalueVisitor : AstVisitor
@ -3837,7 +3858,6 @@ struct Compiler
uint8_t reg = 0;
bool allocated = false;
bool captured = false;
bool skipped = false;
uint32_t debugpc = 0;
};
@ -3858,7 +3878,10 @@ struct Compiler
size_t localOffset;
size_t localOffsetContinue;
// TODO: Remove with LuauCompileFixContinueValidation2
AstExpr* untilCondition;
AstStatContinue* continueUsed;
};
struct InlineArg

View file

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

View file

@ -16,6 +16,8 @@ struct JsonEncoderFixture
Allocator allocator;
AstNameTable names{allocator};
ScopedFastFlag sff{"LuauClipExtraHasEndProps", true};
ParseResult parse(std::string_view src)
{
ParseOptions opts;
@ -91,6 +93,8 @@ TEST_CASE("basic_escaping")
TEST_CASE("encode_AstStatBlock")
{
ScopedFastFlag sff{"LuauClipExtraHasEndProps", true};
AstLocal astlocal{AstName{"a_local"}, Location(), nullptr, 0, 0, nullptr};
AstLocal* astlocalarray[] = {&astlocal};
@ -103,9 +107,9 @@ TEST_CASE("encode_AstStatBlock")
AstStatBlock block{Location(), bodyArray};
CHECK_EQ(
(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));
CHECK(
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")
@ -123,7 +127,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_tables")
CHECK(
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")
@ -135,7 +139,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_table_array")
CHECK(
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")
@ -147,7 +151,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_table_indexer")
CHECK(
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")
@ -243,7 +247,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstExprFunction")
AstExpr* expr = expectParseExpr("function (a) return a end");
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);
}
@ -311,7 +315,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatIf")
AstStat* statement = expectParseStatement("if true then else end");
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);
}
@ -321,7 +325,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatWhile")
AstStat* statement = expectParseStatement("while true do end");
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);
}
@ -331,7 +335,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatRepeat")
AstStat* statement = expectParseStatement("repeat until true");
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);
}
@ -341,7 +345,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatBreak")
AstStat* statement = expectParseStatement("while true do break end");
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);
}
@ -351,7 +355,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatContinue")
AstStat* statement = expectParseStatement("while true do continue end");
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);
}
@ -361,7 +365,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatFor")
AstStat* statement = expectParseStatement("for a=0,1 do end");
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);
}
@ -371,7 +375,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatForIn")
AstStat* statement = expectParseStatement("for a in b do end");
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);
}
@ -391,7 +395,7 @@ TEST_CASE_FIXTURE(JsonEncoderFixture, "encode_AstStatLocalFunction")
AstStat* statement = expectParseStatement("local function a(b) return end");
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);
}

View file

@ -1911,7 +1911,7 @@ RETURN R0 0
TEST_CASE("LoopContinueIgnoresImplicitConstant")
{
ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true};
ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true};
// this used to crash the compiler :(
CHECK_EQ("\n" + compileFunction0(R"(
@ -1928,7 +1928,7 @@ RETURN R0 0
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
CHECK_EQ("\n" + compileFunction0(R"(
@ -1945,7 +1945,7 @@ RETURN R0 0
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
try
@ -1971,7 +1971,7 @@ until c
TEST_CASE("LoopContinueIgnoresImplicitConstantAfterInline")
{
ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation", true};
ScopedFastFlag luauCompileFixContinueValidation{"LuauCompileFixContinueValidation2", true};
// Inlining might also replace some locals with constants instead of allocating them
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")
{
ScopedFastFlag sff("LuauCompileContinueCloseUpvals", true);

View file

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

View file

@ -174,7 +174,8 @@ AstStatBlock* Fixture::parse(const std::string& source, const ParseOptions& pars
{
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, {});
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;
}
CheckResult Fixture::check(Mode mode, std::string source)
CheckResult Fixture::check(Mode mode, const std::string& source)
{
ModuleName mm = fromString(mainModuleName);
configResolver.defaultConfig.mode = mode;

View file

@ -65,7 +65,7 @@ struct Fixture
// Throws Luau::ParseErrors if the parse fails.
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);
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"(
-- TODO
return function ()
print(table.getn({}))
table.foreach({}, function() end)
@ -1514,6 +1515,35 @@ end
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")
{
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");
}
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")
{
LintResult result = lint(R"(

View file

@ -15,102 +15,32 @@ using namespace Luau;
struct NonStrictTypeCheckerFixture : Fixture
{
ParseResult parse(std::string source)
CheckResult checkNonStrict(const std::string& code)
{
ParseOptions opts;
opts.allowDeclarationSyntax = true;
ScopedFastFlag sff{"LuauCheckedFunctionSyntax", true};
return tryParse(source, opts);
ScopedFastFlag flags[] = {
{"LuauCheckedFunctionSyntax", true},
};
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_CASE_FIXTURE(Fixture, "basic")
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "simple_non_strict")
{
Luau::checkNonStrict(builtinTypes, nullptr);
}
TEST_CASE_FIXTURE(NonStrictTypeCheckerFixture, "parse_top_level_checked_fn")
{
std::string src = R"BUILTIN_SRC(
auto res = checkNonStrict(R"BUILTIN_SRC(
declare function @checked abs(n: number): number
)BUILTIN_SRC";
abs("hi")
)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 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);
// LUAU_REQUIRE_ERRORS(res);
}
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());
}
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();

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

View file

@ -263,14 +263,19 @@ TEST_CASE_FIXTURE(Fixture, "cannot_indirectly_compare_types_that_do_not_have_a_m
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
UninhabitedTypeFamily* utf = get<UninhabitedTypeFamily>(result.errors[0]);
REQUIRE(utf);
REQUIRE_EQ(toString(utf->ty), "lt<a, b>");
}
else
{
GenericError* gen = get<GenericError>(result.errors[0]);
REQUIRE(gen != nullptr);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(gen->message == "Types 'a' and 'b' cannot be compared with < because neither type has a metatable");
else
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")
{
@ -288,14 +293,19 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "cannot_indirectly_compare_types_that_do_not_
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
UninhabitedTypeFamily* utf = get<UninhabitedTypeFamily>(result.errors[0]);
REQUIRE(utf);
REQUIRE_EQ(toString(utf->ty), "lt<M, M>");
}
else
{
GenericError* gen = get<GenericError>(result.errors[0]);
REQUIRE(gen != nullptr);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK(gen->message == "Types 'M' and 'M' cannot be compared with < because neither type's metatable has a '__lt' metamethod");
else
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")
{
@ -727,14 +737,19 @@ TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operato
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
UninhabitedTypeFamily* utf = get<UninhabitedTypeFamily>(result.errors[0]);
REQUIRE(utf);
REQUIRE_EQ(toString(utf->ty), "lt<boolean, boolean>");
}
else
{
GenericError* ge = get<GenericError>(result.errors[0]);
REQUIRE(ge);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("Types 'boolean' and 'boolean' cannot be compared with relational operator <", ge->message);
else
CHECK_EQ("Type 'boolean' cannot be compared with relational operator <", ge->message);
}
}
TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operators2")
{
@ -746,13 +761,19 @@ TEST_CASE_FIXTURE(Fixture, "error_on_invalid_operand_types_to_relational_operato
LUAU_REQUIRE_ERROR_COUNT(1, result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
UninhabitedTypeFamily* utf = get<UninhabitedTypeFamily>(result.errors[0]);
REQUIRE(utf);
REQUIRE_EQ(toString(utf->ty), "lt<number | string, number | string>");
}
else
{
GenericError* ge = get<GenericError>(result.errors[0]);
REQUIRE(ge);
if (FFlag::DebugLuauDeferredConstraintResolution)
CHECK_EQ("Types 'number | string' and 'number | string' cannot be compared with relational operator <", ge->message);
else
CHECK_EQ("Type 'number | string' cannot be compared with relational operator <", ge->message);
}
}
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)
{
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
{
@ -947,7 +968,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_subtraction")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
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
{
@ -967,7 +988,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_multiplication")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
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
{
@ -987,7 +1008,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_division")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
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
{
@ -1009,7 +1030,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_floor_division")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
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
{
@ -1029,7 +1050,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_exponentiation")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
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
{
@ -1049,7 +1070,7 @@ TEST_CASE_FIXTURE(Fixture, "infer_type_for_generic_modulo")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
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
{
@ -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")
{
CheckResult result = check(R"(

View file

@ -1099,4 +1099,40 @@ foo(1 :: any)
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();

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
CheckResult result = check(R"(
local a : unknown = nil
local idx, val
local function f(a: unknown)
if typeof(a) == "table" then
for i, v in a do
idx = i
val = v
end
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("unknown", toString(requireType("idx")));
CHECK_EQ("unknown", toString(requireType("val")));
// TODO: they should be `unknown`, not `nil`.
CHECK_EQ("nil", toString(requireType("idx")));
CHECK_EQ("nil", toString(requireType("val")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "conditional_refinement_should_stay_error_suppressing")

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 C = number | boolean
local a: A = 1
local b: B = nil
local c: C = true
local a = 1 :: A
local b = nil :: B
local c = true :: C
local n = 1
local x = a == b
@ -843,7 +843,7 @@ TEST_CASE_FIXTURE(Fixture, "suppress_errors_for_prop_lookup_of_a_union_that_incl
registerHiddenTypes(&frontend);
CheckResult result = check(R"(
local a : err | Not<nil>
local a = 5 :: err | Not<nil>
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.table_annotation
AnnotationTests.two_type_params
AnnotationTests.typeof_expr
AnnotationTests.use_generic_type_alias
AstQuery.last_argument_function_call_type
AstQuery::getDocumentationSymbolAtPosition.class_method
AstQuery::getDocumentationSymbolAtPosition.overloaded_class_method
AstQuery::getDocumentationSymbolAtPosition.table_overloaded_function_prop
AutocompleteTest.anonymous_autofilled_generic_on_argument_type_pack_vararg
AutocompleteTest.anonymous_autofilled_generic_type_pack_vararg
AutocompleteTest.autocomplete_interpolated_string_as_singleton
AutocompleteTest.autocomplete_oop_implicit_self
AutocompleteTest.autocomplete_string_singleton_equality
AutocompleteTest.autocomplete_string_singleton_escape
AutocompleteTest.autocomplete_string_singletons
AutocompleteTest.cyclic_table
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.type_correct_expected_argument_type_pack_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_suggestion_for_overloads
AutocompleteTest.type_correct_suggestion_in_argument
AutocompleteTest.unsealed_table_2
BuiltinTests.aliased_string_format
BuiltinTests.assert_removes_falsy_types
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.coroutine_resume_anything_goes
BuiltinTests.debug_info_is_crazy
BuiltinTests.dont_add_definitions_to_persistent_types
BuiltinTests.global_singleton_types_are_sealed
BuiltinTests.gmatch_capture_types
BuiltinTests.gmatch_capture_types2
@ -59,6 +67,7 @@ BuiltinTests.string_format_as_method
BuiltinTests.string_format_correctly_ordered_types
BuiltinTests.string_format_report_all_type_errors_at_correct_positions
BuiltinTests.string_format_use_correct_argument2
BuiltinTests.string_match
BuiltinTests.table_concat_returns_string
BuiltinTests.table_dot_remove_optionally_returns_generic
BuiltinTests.table_freeze_is_generic
@ -93,10 +102,12 @@ ControlFlowAnalysis.tagged_unions_breaking
ControlFlowAnalysis.tagged_unions_continuing
ControlFlowAnalysis.type_alias_does_not_leak_out_breaking
ControlFlowAnalysis.type_alias_does_not_leak_out_continuing
DefinitionTests.class_definition_function_prop
DefinitionTests.class_definition_indexer
DefinitionTests.class_definition_overload_metamethods
DefinitionTests.class_definition_string_props
DefinitionTests.declaring_generic_functions
DefinitionTests.definition_file_class_function_args
DefinitionTests.definition_file_classes
Differ.equal_generictp_cyclic
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.it_should_be_safe_to_stringify_errors_when_full_type_graph_is_discarded
GenericsTests.apply_type_function_nested_generics1
GenericsTests.apply_type_function_nested_generics3
GenericsTests.apply_type_function_nested_generics2
GenericsTests.better_mismatch_error_messages
GenericsTests.bound_tables_do_not_clone_original_fields
GenericsTests.calling_self_generic_methods
GenericsTests.check_generic_function
GenericsTests.check_generic_local_function
GenericsTests.check_generic_typepack_function
@ -169,11 +181,17 @@ GenericsTests.rank_N_types_via_typeof
GenericsTests.self_recursive_instantiated_param
GenericsTests.type_parameters_can_be_polytypes
GenericsTests.typefuns_sharing_types
IntersectionTypes.argument_is_intersection
IntersectionTypes.error_detailed_intersection_all
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_false_and_bool_and_false
IntersectionTypes.intersect_metatables
IntersectionTypes.intersect_saturate_overloaded_functions
IntersectionTypes.intersection_of_tables
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_3
IntersectionTypes.overloadeded_functions_with_weird_typepacks_4
IntersectionTypes.propagates_name
IntersectionTypes.select_correct_union_fn
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_write_sealed_indirect
IntersectionTypes.union_saturate_overloaded_functions
Linter.DeprecatedApiFenv
Linter.FormatStringTyped
Linter.TableOperationsIndexer
Negations.cofinite_strings_can_be_compared_for_equality
Negations.negated_string_is_a_subtype_of_string
Normalize.higher_order_function_with_annotation
Normalize.negations_of_tables
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.function_returns_many_things_but_first_of_it_is_forgotten
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.it_should_be_agnostic_of_actual_size
ProvisionalTests.luau-polyfill.Array.filter
ProvisionalTests.luau-polyfill.Map.entries
ProvisionalTests.luau_roact_useState_minimization
ProvisionalTests.optional_class_instances_are_invariant
ProvisionalTests.pcall_returns_at_least_two_value_but_function_returns_nothing
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_unification_infinite_recursion
ProvisionalTests.typeguard_inference_incomplete
ProvisionalTests.weirditer_should_not_loop_forever
ProvisionalTests.while_body_are_also_refined
ProvisionalTests.xpcall_returns_what_f_returns
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.dataflow_analysis_can_tell_refinements_when_its_appropriate_to_refine_into_nil_or_never
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.refine_a_property_of_some_global
RefinementTest.refine_unknown_to_table_then_clone_it
RefinementTest.refinements_should_preserve_error_suppression
RefinementTest.string_not_equal_to_string_or_nil
RefinementTest.truthy_constraint_on_properties
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.generalize_table_argument
TableTests.generic_table_instantiation_potential_regression
TableTests.give_up_after_one_metatable_index_look_up
TableTests.indexer_mismatch
TableTests.indexer_on_sealed_table_must_unify_with_free_table
TableTests.indexers_get_quantified_too
TableTests.indexing_from_a_table_should_prefer_properties_when_possible
TableTests.inequality_operators_imply_exactly_matching_types
TableTests.infer_array_2
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_variable_type_and_unifiable
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.instantiate_table_cloning_3
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_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.meta_add_both_ways
TableTests.metatable_mismatch_should_fail
TableTests.missing_metatable_for_sealed_tables_do_not_get_inferred
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.reasonable_error_when_adding_a_nonexistent_property_to_an_array_like_table
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.scalar_is_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.table_call_metamethod_basic
TableTests.table_call_metamethod_generic
TableTests.table_call_metamethod_must_be_callable
TableTests.table_function_check_use_after_free
TableTests.table_insert_should_cope_with_optional_properties_in_strict
TableTests.table_param_width_subtyping_1
@ -354,6 +393,7 @@ TableTests.table_unification_4
TableTests.table_unifies_into_map
TableTests.top_table_type
TableTests.type_mismatch_on_massive_table_is_cut_short
TableTests.unification_of_unions_in_a_self_referential_type
TableTests.unifying_tables_shouldnt_uaf1
TableTests.used_colon_instead_of_dot
TableTests.used_dot_instead_of_colon
@ -362,11 +402,16 @@ ToDot.function
ToString.exhaustive_toString_of_cyclic_table
ToString.free_types
ToString.named_metatable_toStringNamedFunction
ToString.no_parentheses_around_cyclic_function_type_in_union
ToString.pick_distinct_names_for_mixed_explicit_and_implicit_generics
ToString.primitive
ToString.tostring_error_mismatch
ToString.tostring_unsee_ttv_if_array
ToString.toStringDetailed2
ToString.toStringErrorPack
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.result_of_failed_typepack_unification_is_constrained
TryUnifyTests.typepack_unification_should_trim_free_tails
@ -374,6 +419,9 @@ TryUnifyTests.uninhabited_table_sub_anything
TryUnifyTests.uninhabited_table_sub_never
TryUnifyTests.variadics_should_use_reversed_properly
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.free_variables_from_typeof_in_aliases
TypeAliases.generic_param_remap
@ -385,6 +433,7 @@ TypeAliases.mutually_recursive_types_restriction_not_ok_2
TypeAliases.mutually_recursive_types_swapsies_not_ok
TypeAliases.recursive_types_restriction_not_ok
TypeAliases.report_shadowed_aliases
TypeAliases.saturate_to_first_type_pack
TypeAliases.stringify_optional_parameterized_alias
TypeAliases.type_alias_local_mutation
TypeAliases.type_alias_local_rename
@ -404,10 +453,10 @@ TypeInfer.check_expr_recursion_limit
TypeInfer.check_type_infer_recursion_count
TypeInfer.cli_39932_use_unifier_in_ensure_methods
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_AstStatError
TypeInfer.follow_on_new_types_in_substitution
TypeInfer.fuzz_free_table_type_change_during_index_check
TypeInfer.globals_are_banned_in_strict_mode
TypeInfer.if_statement
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.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.stringify_nested_unions_with_optionals
TypeInfer.tc_after_error_recovery_no_replacement_name_in_error
TypeInfer.tc_error
TypeInfer.tc_error_2
TypeInfer.tc_if_else_expressions_expected_type_3
TypeInfer.type_infer_recursion_limit_no_ice
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.for_in_loop_iterator_is_any
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_any2
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.type_error_addition
TypeInferClasses.callable_classes
TypeInferClasses.can_read_prop_of_base_class_using_string
TypeInferClasses.cannot_unify_class_instance_with_primitive
TypeInferClasses.class_type_mismatch_with_name_conflict
TypeInferClasses.class_unification_type_mismatch_is_correct_order
TypeInferClasses.detailed_class_unification_error
TypeInferClasses.index_instance_property
TypeInferClasses.indexable_classes
TypeInferClasses.optional_class_field_access_error
TypeInferClasses.table_class_unification_reports_sane_errors_for_missing_properties
TypeInferClasses.table_indexers_are_invariant
TypeInferClasses.type_mismatch_invariance_required_for_error
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.cannot_hoist_interior_defns_into_signature
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_value_type
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.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.no_lossy_function_type
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_2
TypeInferFunctions.record_matching_overload
TypeInferFunctions.recursive_local_function
TypeInferFunctions.report_exiting_without_return_strict
TypeInferFunctions.return_type_by_overload
TypeInferFunctions.too_few_arguments_variadic
@ -501,6 +563,10 @@ TypeInferFunctions.too_many_return_values_no_function
TypeInferFunctions.vararg_function_is_quantified
TypeInferLoops.cli_68448_iterators_need_not_accept_nil
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_xpath_candidates
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_incompatible_args_to_iterator
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_just_one_iterator_is_ok
TypeInferLoops.for_loop
@ -530,13 +597,17 @@ TypeInferLoops.unreachable_code_after_infinite_loop
TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free
TypeInferLoops.while_loop
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_5
TypeInferModules.general_require_call_expression
TypeInferModules.module_type_conflict
TypeInferModules.module_type_conflict_instantiated
TypeInferModules.require
TypeInferModules.require_failed_module
TypeInferOOP.CheckMethodsOfSealed
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_not_defined_with_colon
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_string_lhs_and_free_rhs
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.normalize_strings_comparison
TypeInferOperators.operator_eq_completely_incompatible
TypeInferOperators.mm_comparisons_must_return_a_boolean
TypeInferOperators.operator_eq_verifies_types_do_intersect
TypeInferOperators.reducing_and
TypeInferOperators.refine_and_or
TypeInferOperators.reworked_and
TypeInferOperators.reworked_or
TypeInferOperators.strict_binary_op_where_lhs_unknown
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection_on_rhs
TypeInferOperators.typecheck_unary_len_error
TypeInferOperators.typecheck_unary_minus
TypeInferOperators.typecheck_unary_minus_error
TypeInferOperators.unrelated_classes_cannot_be_compared
TypeInferOperators.unrelated_primitives_cannot_be_compared
TypeInferPrimitives.CheckMethodsOfNumber
TypeInferPrimitives.string_function_indirect
TypeInferPrimitives.string_index
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_sorta_never
TypeInferUnknownNever.length_of_never
TypeInferUnknownNever.math_operators_and_never
TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable
TypePackTests.fuzz_typepack_iter_follow_2
TypePackTests.pack_tail_unification_check
TypePackTests.parenthesized_varargs_returns_any
TypePackTests.type_alias_backwards_compatible
TypePackTests.type_alias_default_type_errors
TypePackTests.type_alias_type_packs_import
TypePackTests.type_packs_with_tails_in_vararg_adjustment
TypePackTests.unify_variadic_tails_in_arguments
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_call_with_singletons
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.table_properties_singleton_strings
TypeSingletons.table_properties_type_error_escapes
@ -605,12 +680,24 @@ UnionTypes.error_detailed_union_all
UnionTypes.error_detailed_union_part
UnionTypes.generic_function_with_optional_arg
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.optional_arguments_table2
UnionTypes.optional_assignment_errors
UnionTypes.optional_call_error
UnionTypes.optional_field_access_error
UnionTypes.optional_index_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.unify_sealed_table_union_check
UnionTypes.union_of_functions
UnionTypes.union_of_functions_mentioning_generic_typepacks
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_variadics
UnionTypes.union_true_and_false
VisitType.throw_when_limit_is_exceeded