Sync to upstream/release/555 (#768)

* Type mismatch errors now mention if unification failed in covariant or
invariant context, to explain why sometimes derived class can't be
converted to base class or why `T` can't be converted into `T?` and so
on
* Class type indexing is no longer an error in non-strict mode (still an
error in strict mode)
* Fixed cyclic type packs not being displayed in the type
* Added an error when unrelated types are compared with `==`/`~=`
* Fixed false positive errors involving sub-type tests an `never` type
* Fixed miscompilation of multiple assignment statements (Fixes
https://github.com/Roblox/luau/issues/754)
* Type inference stability improvements
This commit is contained in:
vegorov-rbx 2022-12-02 10:09:59 -08:00 committed by GitHub
parent 91302d1d4c
commit 59ae47db43
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
72 changed files with 2313 additions and 1036 deletions

View File

@ -40,6 +40,7 @@ TypeId makeFunction( // Polymorphic
void attachMagicFunction(TypeId ty, MagicFunction fn);
void attachDcrMagicFunction(TypeId ty, DcrMagicFunction fn);
void attachDcrMagicRefinement(TypeId ty, DcrMagicRefinement fn);
Property makeProperty(TypeId ty, std::optional<std::string> documentationSymbol = std::nullopt);
void assignPropDocumentationSymbols(TableTypeVar::Props& props, const std::string& baseName);

View File

@ -3,7 +3,6 @@
#include "Luau/Def.h"
#include "Luau/TypedAllocator.h"
#include "Luau/TypeVar.h"
#include "Luau/Variant.h"
#include <memory>
@ -11,6 +10,9 @@
namespace Luau
{
struct TypeVar;
using TypeId = const TypeVar*;
struct Negation;
struct Conjunction;
struct Disjunction;

View File

@ -149,11 +149,15 @@ struct SetPropConstraint
TypeId propType;
};
// result ~ if isSingleton D then ~D else unknown where D = discriminantType
// if negation:
// result ~ if isSingleton D then ~D else unknown where D = discriminantType
// if not negation:
// result ~ if isSingleton D then D else unknown where D = discriminantType
struct SingletonOrTopTypeConstraint
{
TypeId resultType;
TypeId discriminantType;
bool negated;
};
using ConstraintV = Variant<SubtypeConstraint, PackSubtypeConstraint, GeneralizationConstraint, InstantiationConstraint, UnaryConstraint,

View File

@ -4,7 +4,7 @@
#include "Luau/Ast.h"
#include "Luau/Connective.h"
#include "Luau/Constraint.h"
#include "Luau/DataFlowGraphBuilder.h"
#include "Luau/DataFlowGraph.h"
#include "Luau/Module.h"
#include "Luau/ModuleResolver.h"
#include "Luau/NotNull.h"
@ -215,7 +215,7 @@ struct ConstraintGraphBuilder
ScopePtr bodyScope;
};
FunctionSignature checkFunctionSignature(const ScopePtr& parent, AstExprFunction* fn);
FunctionSignature checkFunctionSignature(const ScopePtr& parent, AstExprFunction* fn, std::optional<TypeId> expectedType = {});
/**
* Checks the body of a function expression.

View File

@ -69,9 +69,14 @@ private:
struct InternalErrorReporter* handle;
std::vector<std::unique_ptr<DfgScope>> scopes;
// Does not belong in DataFlowGraphBuilder, but the old solver allows properties to escape the scope they were defined in,
// so we will need to be able to emulate this same behavior here too. We can kill this once we have better flow sensitivity.
DenseHashMap<const Def*, std::unordered_map<std::string, const Def*>> props{nullptr};
DfgScope* childScope(DfgScope* scope);
std::optional<DefId> use(DfgScope* scope, Symbol symbol, AstExpr* e);
DefId use(DefId def, AstExprIndexName* e);
void visit(DfgScope* scope, AstStatBlock* b);
void visitBlockWithoutChildScope(DfgScope* scope, AstStatBlock* b);

View File

@ -5,37 +5,38 @@
#include "Luau/TypedAllocator.h"
#include "Luau/Variant.h"
#include <string>
#include <optional>
namespace Luau
{
using Def = Variant<struct Undefined, struct Phi>;
/**
* We statically approximate a value at runtime using a symbolic value, which we call a Def.
*
* DataFlowGraphBuilder will allocate these defs as a stand-in for some Luau values, and bind them to places that
* can hold a Luau value, and then observes how those defs will commute as it statically evaluate the program.
*
* It must also be noted that defs are a cyclic graph, so it is not safe to recursively traverse into it expecting it to terminate.
*/
struct Def;
using DefId = NotNull<const Def>;
struct FieldMetadata
{
DefId parent;
std::string propName;
};
/**
* A "single-object" value.
* A cell is a "single-object" value.
*
* Leaky implementation note: sometimes "multiple-object" values, but none of which were interesting enough to warrant creating a phi node instead.
* That can happen because there's no point in creating a phi node that points to either resultant in `if math.random() > 0.5 then 5 else "hello"`.
* This might become of utmost importance if we wanted to do some backward reasoning, e.g. if `5` is taken, then `cond` must be `truthy`.
*/
struct Undefined
struct Cell
{
std::optional<struct FieldMetadata> field;
};
/**
* A phi node is a union of defs.
* A phi node is a union of cells.
*
* We need this because we're statically evaluating a program, and sometimes a place may be assigned with
* different defs, and when that happens, we need a special data type that merges in all the defs
* different cells, and when that happens, we need a special data type that merges in all the cells
* that will flow into that specific place. For example, consider this simple program:
*
* ```
@ -56,23 +57,35 @@ struct Phi
std::vector<DefId> operands;
};
template<typename T>
T* getMutable(DefId def)
/**
* We statically approximate a value at runtime using a symbolic value, which we call a Def.
*
* DataFlowGraphBuilder will allocate these defs as a stand-in for some Luau values, and bind them to places that
* can hold a Luau value, and then observes how those defs will commute as it statically evaluate the program.
*
* It must also be noted that defs are a cyclic graph, so it is not safe to recursively traverse into it expecting it to terminate.
*/
struct Def
{
return get_if<T>(def.get());
}
using V = Variant<struct Cell, struct Phi>;
V v;
};
template<typename T>
const T* get(DefId def)
{
return getMutable<T>(def);
return get_if<T>(&def->v);
}
struct DefArena
{
TypedAllocator<Def> allocator;
DefId freshDef();
DefId freshCell();
DefId freshCell(DefId parent, const std::string& prop);
// TODO: implement once we have cases where we need to merge in definitions
// DefId phi(const std::vector<DefId>& defs);
};
} // namespace Luau

View File

@ -7,21 +7,31 @@
#include "Luau/Variant.h"
#include "Luau/TypeArena.h"
LUAU_FASTFLAG(LuauIceExceptionInheritanceChange)
namespace Luau
{
struct TypeError;
struct TypeMismatch
{
enum Context
{
CovariantContext,
InvariantContext
};
TypeMismatch() = default;
TypeMismatch(TypeId wantedType, TypeId givenType);
TypeMismatch(TypeId wantedType, TypeId givenType, std::string reason);
TypeMismatch(TypeId wantedType, TypeId givenType, std::string reason, std::optional<TypeError> error);
TypeMismatch(TypeId wantedType, TypeId givenType, Context context);
TypeMismatch(TypeId wantedType, TypeId givenType, std::string reason, Context context);
TypeMismatch(TypeId wantedType, TypeId givenType, std::string reason, std::optional<TypeError> error, Context context);
TypeId wantedType = nullptr;
TypeId givenType = nullptr;
Context context = CovariantContext;
std::string reason;
std::shared_ptr<TypeError> error;
@ -312,12 +322,33 @@ struct TypePackMismatch
bool operator==(const TypePackMismatch& rhs) const;
};
struct DynamicPropertyLookupOnClassesUnsafe
{
TypeId ty;
bool operator==(const DynamicPropertyLookupOnClassesUnsafe& 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>;
TypesAreUnrelated, NormalizationTooComplex, TypePackMismatch, DynamicPropertyLookupOnClassesUnsafe>;
struct TypeErrorSummary
{
Location location;
ModuleName moduleName;
int code;
TypeErrorSummary(const Location& location, const ModuleName& moduleName, int code)
: location(location)
, moduleName(moduleName)
, code(code)
{
}
};
struct TypeError
{
@ -325,6 +356,7 @@ struct TypeError
ModuleName moduleName;
TypeErrorData data;
static int minCode();
int code() const;
TypeError() = default;
@ -342,6 +374,8 @@ struct TypeError
}
bool operator==(const TypeError& rhs) const;
TypeErrorSummary summary() const;
};
template<typename T>
@ -406,10 +440,4 @@ public:
const std::optional<Location> location;
};
// These two function overloads only exist to facilitate fast flagging a change to InternalCompilerError
// Both functions can be removed when FFlagLuauIceExceptionInheritanceChange is removed and calling code
// can directly throw InternalCompilerError.
[[noreturn]] void throwRuntimeError(const std::string& message);
[[noreturn]] void throwRuntimeError(const std::string& message, const std::string& moduleName);
} // namespace Luau

View File

@ -194,6 +194,8 @@ struct NormalizedFunctionType
struct NormalizedType;
using NormalizedTyvars = std::unordered_map<TypeId, std::unique_ptr<NormalizedType>>;
bool isInhabited_DEPRECATED(const NormalizedType& norm);
// A normalized type is either any, unknown, or one of the form P | T | F | G where
// * P is a union of primitive types (including singletons, classes and the error type)
// * T is a union of table types
@ -328,6 +330,10 @@ public:
bool intersectNormals(NormalizedType& here, const NormalizedType& there, int ignoreSmallerTyvars = -1);
bool intersectNormalWithTy(NormalizedType& here, TypeId there);
// Check for inhabitance
bool isInhabited(TypeId ty, std::unordered_set<TypeId> seen = {});
bool isInhabited(const NormalizedType* norm, std::unordered_set<TypeId> seen = {});
// -------- Convert back from a normalized type to a type
TypeId typeFromNormal(const NormalizedType& norm);
};

View File

@ -59,6 +59,20 @@ struct NotNull
return ptr;
}
template<typename U>
bool operator==(NotNull<U> other) const noexcept
{
return get() == other.get();
}
template<typename U>
bool operator!=(NotNull<U> other) const noexcept
{
return get() != other.get();
}
operator bool() const noexcept = delete;
T& operator[](int) = delete;
T& operator+(int) = delete;

View File

@ -15,16 +15,6 @@ struct RecursionLimitException : public InternalCompilerError
RecursionLimitException()
: InternalCompilerError("Internal recursion counter limit exceeded")
{
LUAU_ASSERT(FFlag::LuauIceExceptionInheritanceChange);
}
};
struct RecursionLimitException_DEPRECATED : public std::exception
{
const char* what() const noexcept
{
LUAU_ASSERT(!FFlag::LuauIceExceptionInheritanceChange);
return "Internal recursion counter limit exceeded";
}
};
@ -53,14 +43,7 @@ struct RecursionLimiter : RecursionCounter
{
if (limit > 0 && *count > limit)
{
if (FFlag::LuauIceExceptionInheritanceChange)
{
throw RecursionLimitException();
}
else
{
throw RecursionLimitException_DEPRECATED();
}
throw RecursionLimitException();
}
}
};

View File

@ -30,7 +30,7 @@ struct ToStringOptions
bool hideTableKind = false; // If true, all tables will be surrounded with plain '{}'
bool hideNamedFunctionTypeParameters = false; // If true, type parameters of functions will be hidden at top-level.
bool hideFunctionSelfArgument = false; // If true, `self: X` will be omitted from the function signature if the function has self
bool indent = false;
bool DEPRECATED_indent = false; // TODO Deprecated field, prune when clipping flag FFlagLuauLineBreaksDeterminIndents
size_t maxTableLength = size_t(FInt::LuauTableTypeMaximumStringifierLength); // Only applied to TableTypeVars
size_t maxTypeLength = size_t(FInt::LuauTypeMaximumStringifierLength);
ToStringNameMap nameMap;
@ -90,8 +90,6 @@ inline std::string toString(const Constraint& c)
return toString(c, ToStringOptions{});
}
std::string toString(const LValue& lvalue);
std::string toString(const TypeVar& tv, ToStringOptions& opts);
std::string toString(const TypePackVar& tp, ToStringOptions& opts);

View File

@ -108,6 +108,8 @@ struct TxnLog
// If both logs talk about the same type, pack, or table, the rhs takes
// priority.
void concat(TxnLog rhs);
void concatAsIntersections(TxnLog rhs, NotNull<TypeArena> arena);
void concatAsUnion(TxnLog rhs, NotNull<TypeArena> arena);
// Commits the TxnLog, rebinding all type pointers to their pending states.
// Clears the TxnLog afterwards.

View File

@ -54,16 +54,9 @@ public:
explicit TimeLimitError(const std::string& moduleName)
: InternalCompilerError("Typeinfer failed to complete in allotted time", moduleName)
{
LUAU_ASSERT(FFlag::LuauIceExceptionInheritanceChange);
}
};
class TimeLimitError_DEPRECATED : public std::exception
{
public:
virtual const char* what() const throw();
};
// All TypeVars are retained via Environment::typeVars. All TypeIds
// within a program are borrowed pointers into this set.
struct TypeChecker

View File

@ -25,9 +25,9 @@ std::optional<TypeId> getIndexTypeFromType(const ScopePtr& scope, ErrorVec& erro
// Returns the minimum and maximum number of types the argument list can accept.
std::pair<size_t, std::optional<size_t>> getParameterExtents(const TxnLog* log, TypePackId tp, bool includeHiddenVariadics = false);
// "Render" a type pack out to an array of a given length. Expands variadics and
// various other things to get there.
std::vector<TypeId> flatten(TypeArena& arena, NotNull<SingletonTypes> singletonTypes, TypePackId pack, size_t length);
// Extend the provided pack to at least `length` types.
// Returns a temporary TypePack that contains those types plus a tail.
TypePack extendTypePack(TypeArena& arena, NotNull<SingletonTypes> singletonTypes, TypePackId pack, size_t length);
/**
* Reduces a union by decomposing to the any/error type if it appears in the

View File

@ -3,6 +3,8 @@
#include "Luau/Ast.h"
#include "Luau/Common.h"
#include "Luau/Connective.h"
#include "Luau/DataFlowGraph.h"
#include "Luau/DenseHash.h"
#include "Luau/Def.h"
#include "Luau/NotNull.h"
@ -257,7 +259,17 @@ struct MagicFunctionCallContext
TypePackId result;
};
using DcrMagicFunction = std::function<bool(MagicFunctionCallContext)>;
using DcrMagicFunction = bool (*)(MagicFunctionCallContext);
struct MagicRefinementContext
{
ScopePtr scope;
NotNull<const DataFlowGraph> dfg;
NotNull<ConnectiveArena> connectiveArena;
const class AstExprCall* callSite;
};
using DcrMagicRefinement = std::vector<ConnectiveId> (*)(MagicRefinementContext);
struct FunctionTypeVar
{
@ -279,19 +291,20 @@ struct FunctionTypeVar
FunctionTypeVar(TypeLevel level, Scope* scope, std::vector<TypeId> generics, std::vector<TypePackId> genericPacks, TypePackId argTypes,
TypePackId retTypes, std::optional<FunctionDefinition> defn = {}, bool hasSelf = false);
TypeLevel level;
Scope* scope = nullptr;
std::optional<FunctionDefinition> definition;
/// These should all be generic
std::vector<TypeId> generics;
std::vector<TypePackId> genericPacks;
TypePackId argTypes;
std::vector<std::optional<FunctionArgument>> argNames;
TypePackId retTypes;
std::optional<FunctionDefinition> definition;
MagicFunction magicFunction = nullptr; // Function pointer, can be nullptr.
DcrMagicFunction dcrMagicFunction = nullptr; // can be nullptr
bool hasSelf;
Tags tags;
TypeLevel level;
Scope* scope = nullptr;
TypePackId argTypes;
TypePackId retTypes;
MagicFunction magicFunction = nullptr;
DcrMagicFunction dcrMagicFunction = nullptr; // Fired only while solving constraints
DcrMagicRefinement dcrMagicRefinement = nullptr; // Fired only while generating constraints
bool hasSelf;
bool hasNoGenerics = false;
};

View File

@ -120,6 +120,9 @@ private:
std::optional<TypeId> findTablePropertyRespectingMeta(TypeId lhsType, Name name);
TxnLog combineLogsIntoIntersection(std::vector<TxnLog> logs);
TxnLog combineLogsIntoUnion(std::vector<TxnLog> logs);
public:
// Returns true if the type "needle" already occurs within "haystack" and reports an "infinite type error"
bool occursCheck(TypeId needle, TypeId haystack);
@ -134,6 +137,7 @@ public:
private:
bool isNonstrictMode() const;
TypeMismatch::Context mismatchContext();
void checkChildUnifierTypeMismatch(const ErrorVec& innerErrors, TypeId wantedType, TypeId givenType);
void checkChildUnifierTypeMismatch(const ErrorVec& innerErrors, const std::string& prop, TypeId wantedType, TypeId givenType);

View File

@ -11,15 +11,12 @@
#include <algorithm>
LUAU_FASTFLAGVARIABLE(LuauCheckOverloadedDocSymbol, false)
namespace Luau
{
namespace
{
struct AutocompleteNodeFinder : public AstVisitor
{
const Position pos;
@ -432,8 +429,6 @@ ExprOrLocal findExprOrLocalAtPosition(const SourceModule& source, Position pos)
static std::optional<DocumentationSymbol> checkOverloadedDocumentationSymbol(
const Module& module, const TypeId ty, const AstExpr* parentExpr, const std::optional<DocumentationSymbol> documentationSymbol)
{
LUAU_ASSERT(FFlag::LuauCheckOverloadedDocSymbol);
if (!documentationSymbol)
return std::nullopt;
@ -469,40 +464,7 @@ std::optional<DocumentationSymbol> getDocumentationSymbolAtPosition(const Source
AstExpr* parentExpr = ancestry.size() >= 2 ? ancestry[ancestry.size() - 2]->asExpr() : nullptr;
if (std::optional<Binding> binding = findBindingAtPosition(module, source, position))
{
if (FFlag::LuauCheckOverloadedDocSymbol)
{
return checkOverloadedDocumentationSymbol(module, binding->typeId, parentExpr, binding->documentationSymbol);
}
else
{
if (binding->documentationSymbol)
{
// This might be an overloaded function binding.
if (get<IntersectionTypeVar>(follow(binding->typeId)))
{
TypeId matchingOverload = nullptr;
if (parentExpr && parentExpr->is<AstExprCall>())
{
if (auto it = module.astOverloadResolvedTypes.find(parentExpr))
{
matchingOverload = *it;
}
}
if (matchingOverload)
{
std::string overloadSymbol = *binding->documentationSymbol + "/overload/";
// Default toString options are fine for this purpose.
overloadSymbol += toString(matchingOverload);
return overloadSymbol;
}
}
}
return binding->documentationSymbol;
}
}
return checkOverloadedDocumentationSymbol(module, binding->typeId, parentExpr, binding->documentationSymbol);
if (targetExpr)
{
@ -514,22 +476,12 @@ std::optional<DocumentationSymbol> getDocumentationSymbolAtPosition(const Source
if (const TableTypeVar* ttv = get<TableTypeVar>(parentTy))
{
if (auto propIt = ttv->props.find(indexName->index.value); propIt != ttv->props.end())
{
if (FFlag::LuauCheckOverloadedDocSymbol)
return checkOverloadedDocumentationSymbol(module, propIt->second.type, parentExpr, propIt->second.documentationSymbol);
else
return propIt->second.documentationSymbol;
}
return checkOverloadedDocumentationSymbol(module, propIt->second.type, parentExpr, propIt->second.documentationSymbol);
}
else if (const ClassTypeVar* ctv = get<ClassTypeVar>(parentTy))
{
if (auto propIt = ctv->props.find(indexName->index.value); propIt != ctv->props.end())
{
if (FFlag::LuauCheckOverloadedDocSymbol)
return checkOverloadedDocumentationSymbol(module, propIt->second.type, parentExpr, propIt->second.documentationSymbol);
else
return propIt->second.documentationSymbol;
}
return checkOverloadedDocumentationSymbol(module, propIt->second.type, parentExpr, propIt->second.documentationSymbol);
}
}
}

View File

@ -1457,7 +1457,8 @@ static AutocompleteResult autocomplete(const SourceModule& sourceModule, const M
return autocompleteExpression(sourceModule, *module, singletonTypes, &typeArena, ancestry, position);
else if (AstStatRepeat* statRepeat = extractStat<AstStatRepeat>(ancestry); statRepeat)
return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement};
else if (AstExprTable* exprTable = parent->as<AstExprTable>(); exprTable && (node->is<AstExprGlobal>() || node->is<AstExprConstantString>() || node->is<AstExprInterpString>()))
else if (AstExprTable* exprTable = parent->as<AstExprTable>();
exprTable && (node->is<AstExprGlobal>() || node->is<AstExprConstantString>() || node->is<AstExprInterpString>()))
{
for (const auto& [kind, key, value] : exprTable->items)
{

View File

@ -132,6 +132,14 @@ void attachDcrMagicFunction(TypeId ty, DcrMagicFunction fn)
LUAU_ASSERT(!"Got a non functional type");
}
void attachDcrMagicRefinement(TypeId ty, DcrMagicRefinement fn)
{
if (auto ftv = getMutable<FunctionTypeVar>(ty))
ftv->dcrMagicRefinement = fn;
else
LUAU_ASSERT(!"Got a non functional type");
}
Property makeProperty(TypeId ty, std::optional<std::string> documentationSymbol)
{
return {

View File

@ -9,7 +9,9 @@
#include "Luau/ModuleResolver.h"
#include "Luau/RecursionCounter.h"
#include "Luau/Scope.h"
#include "Luau/Substitution.h"
#include "Luau/ToString.h"
#include "Luau/TxnLog.h"
#include "Luau/TypeUtils.h"
#include "Luau/TypeVar.h"
@ -191,7 +193,7 @@ static void unionRefinements(const std::unordered_map<DefId, TypeId>& lhs, const
}
static void computeRefinement(const ScopePtr& scope, ConnectiveId connective, std::unordered_map<DefId, TypeId>* refis, bool sense,
NotNull<TypeArena> arena, bool eq, std::vector<SingletonOrTopTypeConstraint>* constraints)
NotNull<TypeArena> arena, bool eq, std::vector<ConstraintV>* constraints)
{
using RefinementMap = std::unordered_map<DefId, TypeId>;
@ -231,10 +233,10 @@ static void computeRefinement(const ScopePtr& scope, ConnectiveId connective, st
TypeId discriminantTy = proposition->discriminantTy;
if (!sense && !eq)
discriminantTy = arena->addType(NegationTypeVar{proposition->discriminantTy});
else if (!sense && eq)
else if (eq)
{
discriminantTy = arena->addType(BlockedTypeVar{});
constraints->push_back(SingletonOrTopTypeConstraint{discriminantTy, proposition->discriminantTy});
constraints->push_back(SingletonOrTopTypeConstraint{discriminantTy, proposition->discriminantTy, !sense});
}
if (auto it = refis->find(proposition->def); it != refis->end())
@ -244,23 +246,43 @@ static void computeRefinement(const ScopePtr& scope, ConnectiveId connective, st
}
}
static std::pair<DefId, TypeId> computeDiscriminantType(NotNull<TypeArena> arena, const ScopePtr& scope, DefId def, TypeId discriminantTy)
{
LUAU_ASSERT(get<Cell>(def));
while (const Cell* current = get<Cell>(def))
{
if (!current->field)
break;
TableTypeVar::Props props{{current->field->propName, Property{discriminantTy}}};
discriminantTy = arena->addType(TableTypeVar{std::move(props), std::nullopt, TypeLevel{}, scope.get(), TableState::Sealed});
def = current->field->parent;
current = get<Cell>(def);
}
return {def, discriminantTy};
}
void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location location, ConnectiveId connective)
{
if (!connective)
return;
std::unordered_map<DefId, TypeId> refinements;
std::vector<SingletonOrTopTypeConstraint> constraints;
std::vector<ConstraintV> constraints;
computeRefinement(scope, connective, &refinements, /*sense*/ true, arena, /*eq*/ false, &constraints);
for (auto [def, discriminantTy] : refinements)
{
std::optional<TypeId> defTy = scope->lookup(def);
auto [def2, discriminantTy2] = computeDiscriminantType(arena, scope, def, discriminantTy);
std::optional<TypeId> defTy = scope->lookup(def2);
if (!defTy)
ice->ice("Every DefId must map to a type!");
TypeId resultTy = arena->addType(IntersectionTypeVar{{*defTy, discriminantTy}});
scope->dcrRefinements[def] = resultTy;
TypeId resultTy = arena->addType(IntersectionTypeVar{{*defTy, discriminantTy2}});
scope->dcrRefinements[def2] = resultTy;
}
for (auto& c : constraints)
@ -446,15 +468,15 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocal* local)
if (i < local->vars.size)
{
std::vector<TypeId> packTypes = flatten(*arena, singletonTypes, exprPack, varTypes.size() - i);
TypePack packTypes = extendTypePack(*arena, singletonTypes, 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.size())
varTypes[j] = packTypes[j - i];
if (j - i < packTypes.head.size())
varTypes[j] = packTypes.head[j - i];
else
varTypes[j] = freshType(scope);
}
@ -591,9 +613,9 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatLocalFunction*
FunctionSignature sig = checkFunctionSignature(scope, function->func);
sig.bodyScope->bindings[function->name] = Binding{sig.signature, function->func->location};
auto start = checkpoint(this);
Checkpoint start = checkpoint(this);
checkFunctionBody(sig.bodyScope, function->func);
auto end = checkpoint(this);
Checkpoint end = checkpoint(this);
NotNull<Scope> constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()};
std::unique_ptr<Constraint> c =
@ -611,7 +633,7 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction* funct
// Name could be AstStatLocal, AstStatGlobal, AstStatIndexName.
// With or without self
TypeId functionType = nullptr;
TypeId generalizedType = arena->addType(BlockedTypeVar{});
FunctionSignature sig = checkFunctionSignature(scope, function->func);
@ -620,62 +642,59 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatFunction* funct
std::optional<TypeId> existingFunctionTy = scope->lookup(localName->local);
if (existingFunctionTy)
{
// Duplicate definition
functionType = *existingFunctionTy;
addConstraint(scope, function->name->location, SubtypeConstraint{generalizedType, *existingFunctionTy});
Symbol sym{localName->local};
std::optional<DefId> def = dfg->getDef(sym);
LUAU_ASSERT(def);
scope->bindings[sym].typeId = generalizedType;
scope->dcrRefinements[*def] = generalizedType;
}
else
{
functionType = arena->addType(BlockedTypeVar{});
scope->bindings[localName->local] = Binding{functionType, localName->location};
}
scope->bindings[localName->local] = Binding{generalizedType, localName->location};
sig.bodyScope->bindings[localName->local] = Binding{sig.signature, localName->location};
}
else if (AstExprGlobal* globalName = function->name->as<AstExprGlobal>())
{
std::optional<TypeId> existingFunctionTy = scope->lookup(globalName->name);
if (existingFunctionTy)
{
// Duplicate definition
functionType = *existingFunctionTy;
}
else
{
functionType = arena->addType(BlockedTypeVar{});
rootScope->bindings[globalName->name] = Binding{functionType, globalName->location};
}
if (!existingFunctionTy)
ice->ice("prepopulateGlobalScope did not populate a global name", globalName->location);
generalizedType = *existingFunctionTy;
sig.bodyScope->bindings[globalName->name] = Binding{sig.signature, globalName->location};
}
else if (AstExprIndexName* indexName = function->name->as<AstExprIndexName>())
{
TypeId containingTableType = check(scope, indexName->expr).ty;
functionType = arena->addType(BlockedTypeVar{});
// TODO look into stack utilization. This is probably ok because it scales with AST depth.
TypeId prospectiveTableType = arena->addType(TableTypeVar{TableState::Unsealed, TypeLevel{}, scope.get()});
NotNull<TableTypeVar> prospectiveTable{getMutable<TableTypeVar>(prospectiveTableType)};
Property& prop = prospectiveTable->props[indexName->index.value];
prop.type = functionType;
prop.type = generalizedType;
prop.location = function->name->location;
addConstraint(scope, indexName->location, SubtypeConstraint{containingTableType, prospectiveTableType});
}
else if (AstExprError* err = function->name->as<AstExprError>())
{
functionType = singletonTypes->errorRecoveryType();
generalizedType = singletonTypes->errorRecoveryType();
}
LUAU_ASSERT(functionType != nullptr);
if (generalizedType == nullptr)
ice->ice("generalizedType == nullptr", function->location);
auto start = checkpoint(this);
Checkpoint start = checkpoint(this);
checkFunctionBody(sig.bodyScope, function->func);
auto end = checkpoint(this);
Checkpoint end = checkpoint(this);
NotNull<Scope> constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()};
std::unique_ptr<Constraint> c =
std::make_unique<Constraint>(constraintScope, function->name->location, GeneralizationConstraint{functionType, sig.signature});
std::make_unique<Constraint>(constraintScope, function->name->location, GeneralizationConstraint{generalizedType, sig.signature});
forEachConstraint(start, end, this, [&c](const ConstraintPtr& constraint) {
c->dependencies.push_back(NotNull{constraint.get()});
@ -708,7 +727,9 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatBlock* block)
void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatAssign* assign)
{
TypePackId varPackId = checkLValues(scope, assign->vars);
TypePackId valuePack = checkPack(scope, assign->values).tp;
TypePack expectedTypes = extendTypePack(*arena, singletonTypes, varPackId, assign->values.size);
TypePackId valuePack = checkPack(scope, assign->values, expectedTypes.head).tp;
addConstraint(scope, assign->location, PackSubtypeConstraint{valuePack, varPackId});
}
@ -729,8 +750,6 @@ void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatCompoundAssign*
void ConstraintGraphBuilder::visit(const ScopePtr& scope, AstStatIf* ifStatement)
{
// TODO: Optimization opportunity, the interior scope of the condition could be
// reused for the then body, so we don't need to refine twice.
ScopePtr condScope = childScope(ifStatement->condition, scope);
auto [_, connective] = check(condScope, ifStatement->condition, std::nullopt);
@ -986,7 +1005,7 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExpr*
InferencePack result;
if (AstExprCall* call = expr->as<AstExprCall>())
result = {checkPack(scope, call, expectedTypes)};
result = checkPack(scope, call, expectedTypes);
else if (AstExprVarargs* varargs = expr->as<AstExprVarargs>())
{
if (scope->varargPack)
@ -1010,38 +1029,101 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExpr*
InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCall* call, const std::vector<TypeId>& expectedTypes)
{
TypeId fnType = check(scope, call->func).ty;
auto startCheckpoint = checkpoint(this);
std::vector<TypeId> args;
for (AstExpr* arg : call->args)
{
args.push_back(check(scope, arg).ty);
}
std::vector<AstExpr*> exprArgs;
if (call->self)
{
AstExprIndexName* indexExpr = call->func->as<AstExprIndexName>();
if (!indexExpr)
ice->ice("method call expression has no 'self'");
// The call to `check` we already did on `call->func` should have already produced a type for
// `indexExpr->expr`, so we can get it from `astTypes` to avoid exponential blow-up.
TypeId selfType = astTypes[indexExpr->expr];
// If we don't have a type for self, it means we had a code too complex error already.
if (selfType == nullptr)
selfType = singletonTypes->errorRecoveryType();
args.insert(args.begin(), selfType);
exprArgs.push_back(indexExpr->expr);
}
exprArgs.insert(exprArgs.end(), call->args.begin(), call->args.end());
Checkpoint startCheckpoint = checkpoint(this);
TypeId fnType = check(scope, call->func).ty;
Checkpoint fnEndCheckpoint = checkpoint(this);
TypePackId expectedArgPack = arena->freshTypePack(scope.get());
TypePackId expectedRetPack = arena->freshTypePack(scope.get());
TypeId expectedFunctionType = arena->addType(FunctionTypeVar{expectedArgPack, expectedRetPack});
TypeId instantiatedFnType = arena->addType(BlockedTypeVar{});
addConstraint(scope, call->location, InstantiationConstraint{instantiatedFnType, fnType});
NotNull<Constraint> extractArgsConstraint = addConstraint(scope, call->location, SubtypeConstraint{instantiatedFnType, expectedFunctionType});
// Fully solve fnType, then extract its argument list as expectedArgPack.
forEachConstraint(startCheckpoint, fnEndCheckpoint, this, [extractArgsConstraint](const ConstraintPtr& constraint) {
extractArgsConstraint->dependencies.emplace_back(constraint.get());
});
const AstExpr* lastArg = exprArgs.size() ? exprArgs[exprArgs.size() - 1] : nullptr;
const bool needTail = lastArg && (lastArg->is<AstExprCall>() || lastArg->is<AstExprVarargs>());
TypePack expectedArgs;
if (!needTail)
expectedArgs = extendTypePack(*arena, singletonTypes, expectedArgPack, exprArgs.size());
else
expectedArgs = extendTypePack(*arena, singletonTypes, expectedArgPack, exprArgs.size() - 1);
std::vector<ConnectiveId> connectives;
if (auto ftv = get<FunctionTypeVar>(follow(fnType)); ftv && ftv->dcrMagicRefinement)
{
MagicRefinementContext ctx{globalScope, dfg, NotNull{&connectiveArena}, call};
connectives = ftv->dcrMagicRefinement(ctx);
}
std::vector<TypeId> args;
std::optional<TypePackId> argTail;
Checkpoint argCheckpoint = checkpoint(this);
for (size_t i = 0; i < exprArgs.size(); ++i)
{
AstExpr* arg = exprArgs[i];
std::optional<TypeId> expectedType;
if (i < expectedArgs.head.size())
expectedType = expectedArgs.head[i];
if (i == 0 && call->self)
{
// The self type has already been computed as a side effect of
// computing fnType. If computing that did not cause us to exceed a
// recursion limit, we can fetch it from astTypes rather than
// recomputing it.
TypeId* selfTy = astTypes.find(exprArgs[0]);
if (selfTy)
args.push_back(*selfTy);
else
args.push_back(arena->freshType(scope.get()));
}
else if (i < exprArgs.size() - 1 || !(arg->is<AstExprCall>() || arg->is<AstExprVarargs>()))
args.push_back(check(scope, arg, expectedType).ty);
else
argTail = checkPack(scope, arg, {}).tp; // FIXME? not sure about expectedTypes here
}
Checkpoint argEndCheckpoint = checkpoint(this);
// Do not solve argument constraints until after we have extracted the
// expected types from the callable.
forEachConstraint(argCheckpoint, argEndCheckpoint, this, [extractArgsConstraint](const ConstraintPtr& constraint) {
constraint->dependencies.push_back(extractArgsConstraint);
});
if (matchSetmetatable(*call))
{
LUAU_ASSERT(args.size() == 2);
TypeId target = args[0];
TypeId mt = args[1];
TypePack argTailPack;
if (argTail && args.size() < 2)
argTailPack = extendTypePack(*arena, singletonTypes, *argTail, 2 - args.size());
LUAU_ASSERT(args.size() + argTailPack.head.size() == 2);
TypeId target = args.size() > 0 ? args[0] : argTailPack.head[0];
TypeId mt = args.size() > 1 ? args[1] : argTailPack.head[args.size() == 0 ? 1 : 0];
AstExpr* targetExpr = call->args.data[0];
@ -1051,18 +1133,16 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa
if (AstExprLocal* targetLocal = targetExpr->as<AstExprLocal>())
scope->bindings[targetLocal->local].typeId = resultTy;
return InferencePack{arena->addTypePack({resultTy})};
return InferencePack{arena->addTypePack({resultTy}), std::move(connectives)};
}
else
{
auto endCheckpoint = checkpoint(this);
astOriginalCallTypes[call->func] = fnType;
TypeId instantiatedType = arena->addType(BlockedTypeVar{});
// TODO: How do expectedTypes play into this? Do they?
TypePackId rets = arena->addTypePack(BlockedTypePack{});
TypePackId argPack = arena->addTypePack(TypePack{args, {}});
TypePackId argPack = arena->addTypePack(TypePack{args, argTail});
FunctionTypeVar ftv(TypeLevel{}, scope.get(), argPack, rets);
TypeId inferredFnType = arena->addType(ftv);
@ -1071,19 +1151,10 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa
NotNull<const Constraint> ic(unqueuedConstraints.back().get());
unqueuedConstraints.push_back(
std::make_unique<Constraint>(NotNull{scope.get()}, call->func->location, SubtypeConstraint{inferredFnType, instantiatedType}));
std::make_unique<Constraint>(NotNull{scope.get()}, call->func->location, SubtypeConstraint{instantiatedType, inferredFnType}));
NotNull<Constraint> sc(unqueuedConstraints.back().get());
// We force constraints produced by checking function arguments to wait
// until after we have resolved the constraint on the function itself.
// This ensures, for instance, that we start inferring the contents of
// lambdas under the assumption that their arguments and return types
// will be compatible with the enclosing function call.
forEachConstraint(startCheckpoint, endCheckpoint, this, [sc](const ConstraintPtr& constraint) {
constraint->dependencies.push_back(sc);
});
addConstraint(scope, call->func->location,
NotNull<Constraint> fcc = addConstraint(scope, call->func->location,
FunctionCallConstraint{
{ic, sc},
fnType,
@ -1092,7 +1163,16 @@ InferencePack ConstraintGraphBuilder::checkPack(const ScopePtr& scope, AstExprCa
call,
});
return InferencePack{rets};
// We force constraints produced by checking function arguments to wait
// until after we have resolved the constraint on the function itself.
// This ensures, for instance, that we start inferring the contents of
// lambdas under the assumption that their arguments and return types
// will be compatible with the enclosing function call.
forEachConstraint(fnEndCheckpoint, argEndCheckpoint, this, [fcc](const ConstraintPtr& constraint) {
fcc->dependencies.emplace_back(constraint.get());
});
return InferencePack{rets, std::move(connectives)};
}
}
@ -1133,9 +1213,19 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExpr* expr, st
}
else if (auto a = expr->as<AstExprFunction>())
{
FunctionSignature sig = checkFunctionSignature(scope, a);
Checkpoint startCheckpoint = checkpoint(this);
FunctionSignature sig = checkFunctionSignature(scope, a, expectedType);
checkFunctionBody(sig.bodyScope, a);
return Inference{sig.signature};
Checkpoint endCheckpoint = checkpoint(this);
TypeId generalizedTy = arena->addType(BlockedTypeVar{});
NotNull<Constraint> gc = addConstraint(scope, expr->location, GeneralizationConstraint{generalizedTy, sig.signature});
forEachConstraint(startCheckpoint, endCheckpoint, this, [gc](const ConstraintPtr& constraint) {
gc->dependencies.emplace_back(constraint.get());
});
return Inference{generalizedTy};
}
else if (auto indexName = expr->as<AstExprIndexName>())
result = check(scope, indexName);
@ -1253,10 +1343,83 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* gl
return Inference{singletonTypes->errorRecoveryType()};
}
static std::optional<TypeId> lookupProp(TypeId ty, const std::string& propName, NotNull<TypeArena> arena)
{
ty = follow(ty);
if (auto ctv = get<ClassTypeVar>(ty))
{
if (auto prop = lookupClassProp(ctv, propName))
return prop->type;
}
else if (auto ttv = get<TableTypeVar>(ty))
{
if (auto it = ttv->props.find(propName); it != ttv->props.end())
return it->second.type;
}
else if (auto utv = get<IntersectionTypeVar>(ty))
{
std::vector<TypeId> types;
for (TypeId ty : utv)
{
if (auto prop = lookupProp(ty, propName, arena))
{
if (std::find(begin(types), end(types), *prop) == end(types))
types.push_back(*prop);
}
else
return std::nullopt;
}
if (types.size() == 1)
return types[0];
else
return arena->addType(IntersectionTypeVar{std::move(types)});
}
else if (auto utv = get<UnionTypeVar>(ty))
{
std::vector<TypeId> types;
for (TypeId ty : utv)
{
if (auto prop = lookupProp(ty, propName, arena))
{
if (std::find(begin(types), end(types), *prop) == end(types))
types.push_back(*prop);
}
else
return std::nullopt;
}
if (types.size() == 1)
return types[0];
else
return arena->addType(UnionTypeVar{std::move(types)});
}
return std::nullopt;
}
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexName* indexName)
{
TypeId obj = check(scope, indexName->expr).ty;
TypeId result = freshType(scope);
// HACK: We need to return the actual type for type refinements so that it can invoke the dcrMagicRefinement function.
TypeId result;
if (auto prop = lookupProp(obj, indexName->index.value, arena))
result = *prop;
else
result = freshType(scope);
std::optional<DefId> def = dfg->getDef(indexName);
if (def)
{
if (auto ty = scope->lookup(*def))
return Inference{*ty, connectiveArena.proposition(*def, singletonTypes->truthyType)};
else
scope->dcrRefinements[*def] = result;
}
TableTypeVar::Props props{{indexName->index.value, Property{result}}};
const std::optional<TableIndexer> indexer;
@ -1266,7 +1429,10 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexName*
addConstraint(scope, indexName->expr->location, SubtypeConstraint{obj, expectedTableType});
return Inference{result};
if (def)
return Inference{result, connectiveArena.proposition(*def, singletonTypes->truthyType)};
else
return Inference{result};
}
Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprIndexExpr* indexExpr)
@ -1555,8 +1721,16 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTable* exp
{
if (auto stringKey = item.key->as<AstExprConstantString>())
{
expectedValueType = arena->addType(BlockedTypeVar{});
addConstraint(scope, item.value->location, HasPropConstraint{*expectedValueType, *expectedType, stringKey->value.data});
ErrorVec errorVec;
std::optional<TypeId> propTy =
findTablePropertyRespectingMeta(singletonTypes, errorVec, follow(*expectedType), stringKey->value.data, item.value->location);
if (propTy)
expectedValueType = propTy;
else
{
expectedValueType = arena->addType(BlockedTypeVar{});
addConstraint(scope, item.value->location, HasPropConstraint{*expectedValueType, *expectedType, stringKey->value.data});
}
}
}
@ -1590,7 +1764,8 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprTable* exp
return Inference{ty};
}
ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionSignature(const ScopePtr& parent, AstExprFunction* fn)
ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionSignature(
const ScopePtr& parent, AstExprFunction* fn, std::optional<TypeId> expectedType)
{
ScopePtr signatureScope = nullptr;
ScopePtr bodyScope = nullptr;
@ -1599,22 +1774,22 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
std::vector<TypeId> genericTypes;
std::vector<TypePackId> genericTypePacks;
if (expectedType)
expectedType = follow(*expectedType);
bool hasGenerics = fn->generics.size > 0 || fn->genericPacks.size > 0;
// If we don't have any generics, we can save some memory and compute by not
// creating the signatureScope, which is only used to scope the declared
// generics properly.
signatureScope = childScope(fn, parent);
// We need to assign returnType before creating bodyScope so that the
// return type gets propogated to bodyScope.
returnType = freshTypePack(signatureScope);
signatureScope->returnType = returnType;
bodyScope = childScope(fn->body, signatureScope);
if (hasGenerics)
{
signatureScope = childScope(fn, parent);
// We need to assign returnType before creating bodyScope so that the
// return type gets propogated to bodyScope.
returnType = freshTypePack(signatureScope);
signatureScope->returnType = returnType;
bodyScope = childScope(fn->body, signatureScope);
std::vector<std::pair<Name, GenericTypeDefinition>> genericDefinitions = createGenerics(signatureScope, fn->generics);
std::vector<std::pair<Name, GenericTypePackDefinition>> genericPackDefinitions = createGenericPacks(signatureScope, fn->genericPacks);
@ -1631,18 +1806,50 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
genericTypePacks.push_back(g.tp);
signatureScope->privateTypePackBindings[name] = g.tp;
}
// Local variable works around an odd gcc 11.3 warning: <anonymous> may be used uninitialized
std::optional<TypeId> none = std::nullopt;
expectedType = none;
}
else
std::vector<TypeId> argTypes;
TypePack expectedArgPack;
const FunctionTypeVar* expectedFunction = expectedType ? get<FunctionTypeVar>(*expectedType) : nullptr;
if (expectedFunction)
{
bodyScope = childScope(fn, parent);
expectedArgPack = extendTypePack(*arena, singletonTypes, expectedFunction->argTypes, fn->args.size);
returnType = freshTypePack(bodyScope);
bodyScope->returnType = returnType;
genericTypes = expectedFunction->generics;
genericTypePacks = expectedFunction->genericPacks;
}
// To eliminate the need to branch on hasGenerics below, we say that the
// signature scope is the body scope when there is no real signature
// scope.
signatureScope = bodyScope;
for (size_t i = 0; i < fn->args.size; ++i)
{
AstLocal* local = fn->args.data[i];
TypeId t = freshType(signatureScope);
argTypes.push_back(t);
signatureScope->bindings[local] = Binding{t, local->location};
TypeId annotationTy = t;
if (local->annotation)
{
annotationTy = resolveType(signatureScope, local->annotation, /* topLevel */ true);
addConstraint(signatureScope, local->annotation->location, SubtypeConstraint{t, annotationTy});
}
else if (i < expectedArgPack.head.size())
{
addConstraint(signatureScope, local->location, SubtypeConstraint{t, expectedArgPack.head[i]});
}
// HACK: This is the one case where the type of the definition will diverge from the type of the binding.
// We need to do this because there are cases where type refinements needs to have the information available
// at constraint generation time.
if (auto def = dfg->getDef(local))
signatureScope->dcrRefinements[*def] = annotationTy;
}
TypePackId varargPack = nullptr;
@ -1654,22 +1861,28 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
TypePackId annotationType = resolveTypePack(signatureScope, fn->varargAnnotation);
varargPack = annotationType;
}
else if (expectedArgPack.tail && get<VariadicTypePack>(*expectedArgPack.tail))
varargPack = *expectedArgPack.tail;
else
{
varargPack = arena->freshTypePack(signatureScope.get());
}
varargPack = singletonTypes->anyTypePack;
signatureScope->varargPack = varargPack;
bodyScope->varargPack = varargPack;
}
else
{
varargPack = arena->addTypePack(VariadicTypePack{singletonTypes->anyType, /*hidden*/ true});
// We do not add to signatureScope->varargPack because ... is not valid
// in functions without an explicit ellipsis.
signatureScope->varargPack = std::nullopt;
bodyScope->varargPack = std::nullopt;
}
LUAU_ASSERT(nullptr != varargPack);
// If there is both an annotation and an expected type, the annotation wins.
// Type checking will sort out any discrepancies later.
if (fn->returnAnnotation)
{
TypePackId annotatedRetType = resolveTypePack(signatureScope, *fn->returnAnnotation);
@ -1680,26 +1893,11 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
LUAU_ASSERT(get<FreeTypePack>(returnType));
asMutable(returnType)->ty.emplace<BoundTypePack>(annotatedRetType);
}
std::vector<TypeId> argTypes;
for (AstLocal* local : fn->args)
else if (expectedFunction)
{
TypeId t = freshType(signatureScope);
argTypes.push_back(t);
signatureScope->bindings[local] = Binding{t, local->location};
if (auto def = dfg->getDef(local))
signatureScope->dcrRefinements[*def] = t;
if (local->annotation)
{
TypeId argAnnotation = resolveType(signatureScope, local->annotation, /* topLevel */ true);
addConstraint(signatureScope, local->annotation->location, SubtypeConstraint{t, argAnnotation});
}
asMutable(returnType)->ty.emplace<BoundTypePack>(expectedFunction->retTypes);
}
// TODO: Vararg annotation.
// TODO: Preserve argument names in the function's type.
FunctionTypeVar actualFunction{TypeLevel{}, parent.get(), arena->addTypePack(argTypes, varargPack), returnType};
@ -1711,11 +1909,14 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS
LUAU_ASSERT(actualFunctionType);
astTypes[fn] = actualFunctionType;
if (expectedType && get<FreeTypeVar>(*expectedType))
{
asMutable(*expectedType)->ty.emplace<BoundTypeVar>(actualFunctionType);
}
return {
/* signature */ actualFunctionType,
// Undo the workaround we made above: if there's no signature scope,
// don't report it.
/* signatureScope */ hasGenerics ? signatureScope : nullptr,
/* signatureScope */ signatureScope,
/* bodyScope */ bodyScope,
};
}

View File

@ -1233,9 +1233,22 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull<const Con
if (isBlocked(subjectType) || get<PendingExpansionTypeVar>(subjectType))
return block(subjectType, constraint);
if (get<FreeTypeVar>(subjectType))
{
TableTypeVar& ttv = asMutable(subjectType)->ty.emplace<TableTypeVar>(TableState::Free, TypeLevel{}, constraint->scope);
ttv.props[c.prop] = Property{c.resultType};
asMutable(c.resultType)->ty.emplace<FreeTypeVar>(constraint->scope);
unblock(c.resultType);
return true;
}
std::optional<TypeId> resultType = lookupTableProp(subjectType, c.prop);
if (!resultType)
return false;
{
asMutable(c.resultType)->ty.emplace<BoundTypeVar>(singletonTypes->errorRecoveryType());
unblock(c.resultType);
return true;
}
if (isBlocked(*resultType))
{
@ -1418,8 +1431,10 @@ bool ConstraintSolver::tryDispatch(const SingletonOrTopTypeConstraint& c, NotNul
TypeId followed = follow(c.discriminantType);
// `nil` is a singleton type too! There's only one value of type `nil`.
if (get<SingletonTypeVar>(followed) || isNil(followed))
if (c.negated && (get<SingletonTypeVar>(followed) || isNil(followed)))
*asMutable(c.resultType) = NegationTypeVar{c.discriminantType};
else if (!c.negated && get<SingletonTypeVar>(followed))
*asMutable(c.resultType) = BoundTypeVar{c.discriminantType};
else
*asMutable(c.resultType) = BoundTypeVar{singletonTypes->unknownType};
@ -1509,17 +1524,17 @@ bool ConstraintSolver::tryDispatchIterableTable(TypeId iteratorTy, const Iterabl
TypePackId expectedIterArgs = arena->addTypePack({iteratorTy});
unify(iterFtv->argTypes, expectedIterArgs, constraint->scope);
std::vector<TypeId> iterRets = flatten(*arena, singletonTypes, iterFtv->retTypes, 2);
TypePack iterRets = extendTypePack(*arena, singletonTypes, iterFtv->retTypes, 2);
if (iterRets.size() < 1)
if (iterRets.head.size() < 1)
{
// We've done what we can; this will get reported as an
// error by the type checker.
return true;
}
TypeId nextFn = iterRets[0];
TypeId table = iterRets.size() == 2 ? iterRets[1] : arena->freshType(constraint->scope);
TypeId nextFn = iterRets.head[0];
TypeId table = iterRets.head.size() == 2 ? iterRets.head[1] : arena->freshType(constraint->scope);
if (std::optional<TypeId> instantiatedNextFn = instantiation.substitute(nextFn))
{

View File

@ -1,5 +1,5 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/DataFlowGraphBuilder.h"
#include "Luau/DataFlowGraph.h"
#include "Luau/Error.h"
@ -11,6 +11,9 @@ namespace Luau
std::optional<DefId> DataFlowGraph::getDef(const AstExpr* expr) const
{
// We need to skip through AstExprGroup because DFG doesn't try its best to transitively
while (auto group = expr->as<AstExprGroup>())
expr = group->expr;
if (auto def = astDefs.find(expr))
return NotNull{*def};
return std::nullopt;
@ -52,16 +55,25 @@ std::optional<DefId> DataFlowGraphBuilder::use(DfgScope* scope, Symbol symbol, A
{
for (DfgScope* current = scope; current; current = current->parent)
{
if (auto loc = current->bindings.find(symbol))
if (auto def = current->bindings.find(symbol))
{
graph.astDefs[e] = *loc;
return NotNull{*loc};
graph.astDefs[e] = *def;
return NotNull{*def};
}
}
return std::nullopt;
}
DefId DataFlowGraphBuilder::use(DefId def, AstExprIndexName* e)
{
auto& propertyDef = props[def][e->index.value];
if (!propertyDef)
propertyDef = arena->freshCell(def, e->index.value);
graph.astDefs[e] = propertyDef;
return NotNull{propertyDef};
}
void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatBlock* b)
{
DfgScope* child = childScope(scope);
@ -180,7 +192,7 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocal* l)
for (AstLocal* local : l->vars)
{
DefId def = arena->freshDef();
DefId def = arena->freshCell();
graph.localDefs[local] = def;
scope->bindings[local] = def;
}
@ -189,7 +201,7 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocal* l)
void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFor* f)
{
DfgScope* forScope = childScope(scope); // TODO: loop scope.
DefId def = arena->freshDef();
DefId def = arena->freshCell();
graph.localDefs[f->var] = def;
scope->bindings[f->var] = def;
@ -203,7 +215,7 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatForIn* f)
for (AstLocal* local : f->vars)
{
DefId def = arena->freshDef();
DefId def = arena->freshCell();
graph.localDefs[local] = def;
forScope->bindings[local] = def;
}
@ -245,7 +257,7 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatAssign* a)
// TODO global?
if (auto exprLocal = root->as<AstExprLocal>())
{
DefId def = arena->freshDef();
DefId def = arena->freshCell();
graph.astDefs[exprLocal] = def;
// Update the def in the scope that introduced the local. Not
@ -277,7 +289,7 @@ void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatFunction* f)
void DataFlowGraphBuilder::visit(DfgScope* scope, AstStatLocalFunction* l)
{
DefId def = arena->freshDef();
DefId def = arena->freshCell();
graph.localDefs[l->name] = def;
scope->bindings[l->name] = def;
@ -354,8 +366,7 @@ ExpressionFlowGraph DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprInde
if (!def)
return {};
// TODO: properties for the above def.
return {};
return {use(*def, i)};
}
ExpressionFlowGraph DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprIndexExpr* i)
@ -375,14 +386,14 @@ ExpressionFlowGraph DataFlowGraphBuilder::visitExpr(DfgScope* scope, AstExprFunc
{
if (AstLocal* self = f->self)
{
DefId def = arena->freshDef();
DefId def = arena->freshCell();
graph.localDefs[self] = def;
scope->bindings[self] = def;
}
for (AstLocal* param : f->args)
{
DefId def = arena->freshDef();
DefId def = arena->freshCell();
graph.localDefs[param] = def;
scope->bindings[param] = def;
}

View File

@ -4,9 +4,14 @@
namespace Luau
{
DefId DefArena::freshDef()
DefId DefArena::freshCell()
{
return NotNull{allocator.allocate<Def>(Undefined{})};
return NotNull{allocator.allocate<Def>(Def{Cell{std::nullopt}})};
}
DefId DefArena::freshCell(DefId parent, const std::string& prop)
{
return NotNull{allocator.allocate<Def>(Def{Cell{FieldMetadata{parent, prop}}})};
}
} // namespace Luau

View File

@ -2,12 +2,14 @@
#include "Luau/Error.h"
#include "Luau/Clone.h"
#include "Luau/Common.h"
#include "Luau/StringUtils.h"
#include "Luau/ToString.h"
#include <stdexcept>
#include <type_traits>
LUAU_FASTFLAGVARIABLE(LuauIceExceptionInheritanceChange, false)
LUAU_FASTFLAGVARIABLE(LuauTypeMismatchInvarianceInError, false)
static std::string wrongNumberOfArgsString(
size_t expectedCount, std::optional<size_t> maximumCount, size_t actualCount, const char* argPrefix = nullptr, bool isVariadic = false)
@ -89,6 +91,7 @@ struct ErrorConverter
if (result.empty())
result = "Type '" + givenTypeName + "' could not be converted into '" + wantedTypeName + "'";
if (tm.error)
{
result += "\ncaused by:\n ";
@ -102,6 +105,10 @@ struct ErrorConverter
{
result += "; " + tm.reason;
}
else if (FFlag::LuauTypeMismatchInvarianceInError && tm.context == TypeMismatch::InvariantContext)
{
result += " in an invariant context";
}
return result;
}
@ -467,6 +474,11 @@ struct ErrorConverter
{
return "Type pack '" + toString(e.givenTp) + "' could not be converted into '" + toString(e.wantedTp) + "'";
}
std::string operator()(const DynamicPropertyLookupOnClassesUnsafe& e) const
{
return "Attempting a dynamic property access on type '" + Luau::toString(e.ty) + "' is unsafe and may cause exceptions at runtime";
}
};
struct InvalidNameChecker
@ -514,6 +526,30 @@ TypeMismatch::TypeMismatch(TypeId wantedType, TypeId givenType, std::string reas
{
}
TypeMismatch::TypeMismatch(TypeId wantedType, TypeId givenType, TypeMismatch::Context context)
: wantedType(wantedType)
, givenType(givenType)
, context(context)
{
}
TypeMismatch::TypeMismatch(TypeId wantedType, TypeId givenType, std::string reason, TypeMismatch::Context context)
: wantedType(wantedType)
, givenType(givenType)
, context(context)
, reason(reason)
{
}
TypeMismatch::TypeMismatch(TypeId wantedType, TypeId givenType, std::string reason, std::optional<TypeError> error, TypeMismatch::Context context)
: wantedType(wantedType)
, givenType(givenType)
, context(context)
, reason(reason)
, error(error ? std::make_shared<TypeError>(std::move(*error)) : nullptr)
{
}
bool TypeMismatch::operator==(const TypeMismatch& rhs) const
{
if (!!error != !!rhs.error)
@ -522,7 +558,7 @@ bool TypeMismatch::operator==(const TypeMismatch& rhs) const
if (error && !(*error == *rhs.error))
return false;
return *wantedType == *rhs.wantedType && *givenType == *rhs.givenType && reason == rhs.reason;
return *wantedType == *rhs.wantedType && *givenType == *rhs.givenType && reason == rhs.reason && context == rhs.context;
}
bool UnknownSymbol::operator==(const UnknownSymbol& rhs) const
@ -662,7 +698,17 @@ bool FunctionExitsWithoutReturning::operator==(const FunctionExitsWithoutReturni
int TypeError::code() const
{
return 1000 + int(data.index());
return minCode() + int(data.index());
}
int TypeError::minCode()
{
return 1000;
}
TypeErrorSummary TypeError::summary() const
{
return TypeErrorSummary{location, moduleName, code()};
}
bool TypeError::operator==(const TypeError& rhs) const
@ -730,6 +776,11 @@ bool TypePackMismatch::operator==(const TypePackMismatch& rhs) const
return *wantedTp == *rhs.wantedTp && *givenTp == *rhs.givenTp;
}
bool DynamicPropertyLookupOnClassesUnsafe::operator==(const DynamicPropertyLookupOnClassesUnsafe& rhs) const
{
return ty == rhs.ty;
}
std::string toString(const TypeError& error)
{
return toString(error, TypeErrorToStringOptions{});
@ -886,6 +937,8 @@ void copyError(T& e, TypeArena& destArena, CloneState cloneState)
e.wantedTp = clone(e.wantedTp);
e.givenTp = clone(e.givenTp);
}
else if constexpr (std::is_same_v<T, DynamicPropertyLookupOnClassesUnsafe>)
e.ty = clone(e.ty);
else
static_assert(always_false_v<T>, "Non-exhaustive type switch");
}
@ -930,30 +983,4 @@ const char* InternalCompilerError::what() const throw()
return this->message.data();
}
// TODO: Inline me when LuauIceExceptionInheritanceChange is deleted.
void throwRuntimeError(const std::string& message)
{
if (FFlag::LuauIceExceptionInheritanceChange)
{
throw InternalCompilerError(message);
}
else
{
throw std::runtime_error(message);
}
}
// TODO: Inline me when LuauIceExceptionInheritanceChange is deleted.
void throwRuntimeError(const std::string& message, const std::string& moduleName)
{
if (FFlag::LuauIceExceptionInheritanceChange)
{
throw InternalCompilerError(message, moduleName);
}
else
{
throw std::runtime_error(message);
}
}
} // namespace Luau

View File

@ -7,7 +7,7 @@
#include "Luau/Config.h"
#include "Luau/ConstraintGraphBuilder.h"
#include "Luau/ConstraintSolver.h"
#include "Luau/DataFlowGraphBuilder.h"
#include "Luau/DataFlowGraph.h"
#include "Luau/DcrLogger.h"
#include "Luau/FileResolver.h"
#include "Luau/Parser.h"
@ -31,7 +31,6 @@ LUAU_FASTINTVARIABLE(LuauAutocompleteCheckTimeoutMs, 100)
LUAU_FASTFLAGVARIABLE(DebugLuauDeferredConstraintResolution, false)
LUAU_FASTFLAG(DebugLuauLogSolverToJson);
LUAU_FASTFLAGVARIABLE(LuauFixMarkDirtyReverseDeps, false)
LUAU_FASTFLAGVARIABLE(LuauPersistTypesAfterGeneratingDocSyms, false)
namespace Luau
{
@ -112,57 +111,32 @@ LoadDefinitionFileResult Frontend::loadDefinitionFile(std::string_view source, c
CloneState cloneState;
if (FFlag::LuauPersistTypesAfterGeneratingDocSyms)
std::vector<TypeId> typesToPersist;
typesToPersist.reserve(checkedModule->declaredGlobals.size() + checkedModule->getModuleScope()->exportedTypeBindings.size());
for (const auto& [name, ty] : checkedModule->declaredGlobals)
{
std::vector<TypeId> typesToPersist;
typesToPersist.reserve(checkedModule->declaredGlobals.size() + checkedModule->getModuleScope()->exportedTypeBindings.size());
TypeId globalTy = clone(ty, globalTypes, cloneState);
std::string documentationSymbol = packageName + "/global/" + name;
generateDocumentationSymbols(globalTy, documentationSymbol);
globalScope->bindings[typeChecker.globalNames.names->getOrAdd(name.c_str())] = {globalTy, Location(), false, {}, documentationSymbol};
for (const auto& [name, ty] : checkedModule->declaredGlobals)
{
TypeId globalTy = clone(ty, globalTypes, cloneState);
std::string documentationSymbol = packageName + "/global/" + name;
generateDocumentationSymbols(globalTy, documentationSymbol);
globalScope->bindings[typeChecker.globalNames.names->getOrAdd(name.c_str())] = {globalTy, Location(), false, {}, documentationSymbol};
typesToPersist.push_back(globalTy);
}
for (const auto& [name, ty] : checkedModule->getModuleScope()->exportedTypeBindings)
{
TypeFun globalTy = clone(ty, globalTypes, cloneState);
std::string documentationSymbol = packageName + "/globaltype/" + name;
generateDocumentationSymbols(globalTy.type, documentationSymbol);
globalScope->exportedTypeBindings[name] = globalTy;
typesToPersist.push_back(globalTy.type);
}
for (TypeId ty : typesToPersist)
{
persist(ty);
}
typesToPersist.push_back(globalTy);
}
else
for (const auto& [name, ty] : checkedModule->getModuleScope()->exportedTypeBindings)
{
for (const auto& [name, ty] : checkedModule->declaredGlobals)
{
TypeId globalTy = clone(ty, globalTypes, cloneState);
std::string documentationSymbol = packageName + "/global/" + name;
generateDocumentationSymbols(globalTy, documentationSymbol);
globalScope->bindings[typeChecker.globalNames.names->getOrAdd(name.c_str())] = {globalTy, Location(), false, {}, documentationSymbol};
TypeFun globalTy = clone(ty, globalTypes, cloneState);
std::string documentationSymbol = packageName + "/globaltype/" + name;
generateDocumentationSymbols(globalTy.type, documentationSymbol);
globalScope->exportedTypeBindings[name] = globalTy;
persist(globalTy);
}
typesToPersist.push_back(globalTy.type);
}
for (const auto& [name, ty] : checkedModule->getModuleScope()->exportedTypeBindings)
{
TypeFun globalTy = clone(ty, globalTypes, cloneState);
std::string documentationSymbol = packageName + "/globaltype/" + name;
generateDocumentationSymbols(globalTy.type, documentationSymbol);
globalScope->exportedTypeBindings[name] = globalTy;
persist(globalTy.type);
}
for (TypeId ty : typesToPersist)
{
persist(ty);
}
return LoadDefinitionFileResult{true, parseResult, checkedModule};
@ -194,57 +168,32 @@ LoadDefinitionFileResult loadDefinitionFile(TypeChecker& typeChecker, ScopePtr t
CloneState cloneState;
if (FFlag::LuauPersistTypesAfterGeneratingDocSyms)
std::vector<TypeId> typesToPersist;
typesToPersist.reserve(checkedModule->declaredGlobals.size() + checkedModule->getModuleScope()->exportedTypeBindings.size());
for (const auto& [name, ty] : checkedModule->declaredGlobals)
{
std::vector<TypeId> typesToPersist;
typesToPersist.reserve(checkedModule->declaredGlobals.size() + checkedModule->getModuleScope()->exportedTypeBindings.size());
TypeId globalTy = clone(ty, typeChecker.globalTypes, cloneState);
std::string documentationSymbol = packageName + "/global/" + name;
generateDocumentationSymbols(globalTy, documentationSymbol);
targetScope->bindings[typeChecker.globalNames.names->getOrAdd(name.c_str())] = {globalTy, Location(), false, {}, documentationSymbol};
for (const auto& [name, ty] : checkedModule->declaredGlobals)
{
TypeId globalTy = clone(ty, typeChecker.globalTypes, cloneState);
std::string documentationSymbol = packageName + "/global/" + name;
generateDocumentationSymbols(globalTy, documentationSymbol);
targetScope->bindings[typeChecker.globalNames.names->getOrAdd(name.c_str())] = {globalTy, Location(), false, {}, documentationSymbol};
typesToPersist.push_back(globalTy);
}
for (const auto& [name, ty] : checkedModule->getModuleScope()->exportedTypeBindings)
{
TypeFun globalTy = clone(ty, typeChecker.globalTypes, cloneState);
std::string documentationSymbol = packageName + "/globaltype/" + name;
generateDocumentationSymbols(globalTy.type, documentationSymbol);
targetScope->exportedTypeBindings[name] = globalTy;
typesToPersist.push_back(globalTy.type);
}
for (TypeId ty : typesToPersist)
{
persist(ty);
}
typesToPersist.push_back(globalTy);
}
else
for (const auto& [name, ty] : checkedModule->getModuleScope()->exportedTypeBindings)
{
for (const auto& [name, ty] : checkedModule->declaredGlobals)
{
TypeId globalTy = clone(ty, typeChecker.globalTypes, cloneState);
std::string documentationSymbol = packageName + "/global/" + name;
generateDocumentationSymbols(globalTy, documentationSymbol);
targetScope->bindings[typeChecker.globalNames.names->getOrAdd(name.c_str())] = {globalTy, Location(), false, {}, documentationSymbol};
TypeFun globalTy = clone(ty, typeChecker.globalTypes, cloneState);
std::string documentationSymbol = packageName + "/globaltype/" + name;
generateDocumentationSymbols(globalTy.type, documentationSymbol);
targetScope->exportedTypeBindings[name] = globalTy;
persist(globalTy);
}
typesToPersist.push_back(globalTy.type);
}
for (const auto& [name, ty] : checkedModule->getModuleScope()->exportedTypeBindings)
{
TypeFun globalTy = clone(ty, typeChecker.globalTypes, cloneState);
std::string documentationSymbol = packageName + "/globaltype/" + name;
generateDocumentationSymbols(globalTy.type, documentationSymbol);
targetScope->exportedTypeBindings[name] = globalTy;
persist(globalTy.type);
}
for (TypeId ty : typesToPersist)
{
persist(ty);
}
return LoadDefinitionFileResult{true, parseResult, checkedModule};
@ -493,13 +442,13 @@ CheckResult Frontend::check(const ModuleName& name, std::optional<FrontendOption
{
auto it2 = moduleResolverForAutocomplete.modules.find(name);
if (it2 == moduleResolverForAutocomplete.modules.end() || it2->second == nullptr)
throwRuntimeError("Frontend::modules does not have data for " + name, name);
throw InternalCompilerError("Frontend::modules does not have data for " + name, name);
}
else
{
auto it2 = moduleResolver.modules.find(name);
if (it2 == moduleResolver.modules.end() || it2->second == nullptr)
throwRuntimeError("Frontend::modules does not have data for " + name, name);
throw InternalCompilerError("Frontend::modules does not have data for " + name, name);
}
return CheckResult{
@ -606,7 +555,7 @@ CheckResult Frontend::check(const ModuleName& name, std::optional<FrontendOption
stats.filesNonstrict += mode == Mode::Nonstrict;
if (module == nullptr)
throwRuntimeError("Frontend::check produced a nullptr module for " + moduleName, moduleName);
throw InternalCompilerError("Frontend::check produced a nullptr module for " + moduleName, moduleName);
if (!frontendOptions.retainFullTypeGraphs)
{

View File

@ -190,6 +190,8 @@ static void errorToString(std::ostream& stream, const T& err)
stream << "NormalizationTooComplex { }";
else if constexpr (std::is_same_v<T, TypePackMismatch>)
stream << "TypePackMismatch { wanted = '" + toString(err.wantedTp) + "', given = '" + toString(err.givenTp) + "' }";
else if constexpr (std::is_same_v<T, DynamicPropertyLookupOnClassesUnsafe>)
stream << "DynamicPropertyLookupOnClassesUnsafe { " << toString(err.ty) << " }";
else
static_assert(always_false_v<T>, "Non-exhaustive type switch");
}

View File

@ -24,6 +24,7 @@ LUAU_FASTFLAGVARIABLE(LuauNegatedFunctionTypes, false);
LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(LuauOverloadedFunctionSubtypingPerf);
LUAU_FASTFLAG(LuauUninhabitedSubAnything)
namespace Luau
{
@ -240,13 +241,75 @@ NormalizedType::NormalizedType(NotNull<SingletonTypes> singletonTypes)
{
}
static bool isInhabited(const NormalizedType& norm)
static bool isShallowInhabited(const NormalizedType& norm)
{
// This test is just a shallow check, for example it returns `true` for `{ p : never }`
return !get<NeverTypeVar>(norm.tops) || !get<NeverTypeVar>(norm.booleans) || !norm.classes.empty() || !get<NeverTypeVar>(norm.errors) ||
!get<NeverTypeVar>(norm.nils) || !get<NeverTypeVar>(norm.numbers) || !norm.strings.isNever() || !get<NeverTypeVar>(norm.threads) ||
!norm.functions.isNever() || !norm.tables.empty() || !norm.tyvars.empty();
}
bool isInhabited_DEPRECATED(const NormalizedType& norm)
{
LUAU_ASSERT(!FFlag::LuauUninhabitedSubAnything);
return isShallowInhabited(norm);
}
bool Normalizer::isInhabited(const NormalizedType* norm, std::unordered_set<TypeId> seen)
{
if (!get<NeverTypeVar>(norm->tops) || !get<NeverTypeVar>(norm->booleans) || !get<NeverTypeVar>(norm->errors) ||
!get<NeverTypeVar>(norm->nils) || !get<NeverTypeVar>(norm->numbers) || !get<NeverTypeVar>(norm->threads) ||
!norm->classes.empty() || !norm->strings.isNever() || !norm->functions.isNever())
return true;
for (const auto& [_, intersect] : norm->tyvars)
{
if (isInhabited(intersect.get(), seen))
return true;
}
for (TypeId table : norm->tables)
{
if (isInhabited(table, seen))
return true;
}
return false;
}
bool Normalizer::isInhabited(TypeId ty, std::unordered_set<TypeId> seen)
{
// TODO: use log.follow(ty), CLI-64291
ty = follow(ty);
if (get<NeverTypeVar>(ty))
return false;
if (!get<IntersectionTypeVar>(ty) && !get<UnionTypeVar>(ty) && !get<TableTypeVar>(ty) && !get<MetatableTypeVar>(ty))
return true;
if (seen.count(ty))
return true;
seen.insert(ty);
if (const TableTypeVar* ttv = get<TableTypeVar>(ty))
{
for (const auto& [_, prop] : ttv->props)
{
if (!isInhabited(prop.type, seen))
return false;
}
return true;
}
if (const MetatableTypeVar* mtv = get<MetatableTypeVar>(ty))
return isInhabited(mtv->table, seen) && isInhabited(mtv->metatable, seen);
const NormalizedType* norm = normalize(ty);
return isInhabited(norm, seen);
}
static int tyvarIndex(TypeId ty)
{
if (const GenericTypeVar* gtv = get<GenericTypeVar>(ty))
@ -378,7 +441,7 @@ static bool isNormalizedTyvar(const NormalizedTyvars& tyvars)
{
if (!isPlainTyvar(tyvar))
return false;
if (!isInhabited(*intersect))
if (!isShallowInhabited(*intersect))
return false;
for (auto& [other, _] : intersect->tyvars)
if (tyvarIndex(other) <= tyvarIndex(tyvar))
@ -1852,7 +1915,7 @@ bool Normalizer::intersectTyvarsWithTy(NormalizedTyvars& here, TypeId there)
NormalizedType& inter = *it->second;
if (!intersectNormalWithTy(inter, there))
return false;
if (isInhabited(inter))
if (isShallowInhabited(inter))
++it;
else
it = here.erase(it);
@ -1914,7 +1977,7 @@ bool Normalizer::intersectNormals(NormalizedType& here, const NormalizedType& th
if (!intersectNormals(inter, *found->second, index))
return false;
}
if (isInhabited(inter))
if (isShallowInhabited(inter))
it++;
else
it = here.tyvars.erase(it);

View File

@ -11,8 +11,8 @@
#include <stdexcept>
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(LuauLvaluelessPath)
LUAU_FASTFLAG(LuauUnknownAndNeverType)
LUAU_FASTFLAGVARIABLE(LuauLineBreaksDetermineIndents, false)
LUAU_FASTFLAGVARIABLE(LuauFunctionReturnStringificationFixup, false)
LUAU_FASTFLAGVARIABLE(LuauUnseeArrayTtv, false)
@ -272,10 +272,20 @@ struct StringifierState
private:
void emitIndentation()
{
if (!opts.indent)
return;
if (!FFlag::LuauLineBreaksDetermineIndents)
{
if (!opts.DEPRECATED_indent)
return;
emit(std::string(indentation, ' '));
emit(std::string(indentation, ' '));
}
else
{
if (!opts.useLineBreaks)
return;
emit(std::string(indentation, ' '));
}
}
};
@ -445,7 +455,7 @@ struct TypeVarStringifier
return;
default:
LUAU_ASSERT(!"Unknown primitive type");
throwRuntimeError("Unknown primitive type " + std::to_string(ptv.type));
throw InternalCompilerError("Unknown primitive type " + std::to_string(ptv.type));
}
}
@ -462,7 +472,7 @@ struct TypeVarStringifier
else
{
LUAU_ASSERT(!"Unknown singleton type");
throwRuntimeError("Unknown singleton type");
throw InternalCompilerError("Unknown singleton type");
}
}
@ -508,24 +518,13 @@ struct TypeVarStringifier
bool plural = true;
if (FFlag::LuauFunctionReturnStringificationFixup)
auto retBegin = begin(ftv.retTypes);
auto retEnd = end(ftv.retTypes);
if (retBegin != retEnd)
{
auto retBegin = begin(ftv.retTypes);
auto retEnd = end(ftv.retTypes);
if (retBegin != retEnd)
{
++retBegin;
if (retBegin == retEnd && !retBegin.tail())
plural = false;
}
}
else
{
if (auto retPack = get<TypePack>(follow(ftv.retTypes)))
{
if (retPack->head.size() == 1 && !retPack->tail)
plural = false;
}
++retBegin;
if (retBegin == retEnd && !retBegin.tail())
plural = false;
}
if (plural)
@ -980,8 +979,6 @@ struct TypePackStringifier
void operator()(TypePackId tp, const GenericTypePack& pack)
{
if (FFlag::DebugLuauVerboseTypeNames)
state.emit("gen-");
if (pack.explicitName)
{
state.usedNames.insert(pack.name);
@ -992,6 +989,15 @@ struct TypePackStringifier
{
state.emit(state.getName(tp));
}
if (FFlag::DebugLuauVerboseTypeNames)
{
state.emit("-");
if (FFlag::DebugLuauDeferredConstraintResolution)
state.emitLevel(pack.scope);
else
state.emit(pack.level);
}
state.emit("...");
}
@ -1143,7 +1149,7 @@ ToStringResult toStringDetailed(TypeId ty, ToStringOptions& opts)
else
tvs.stringify(ty);
if (!state.cycleNames.empty())
if (!state.cycleNames.empty() || !state.cycleTpNames.empty())
{
result.cycle = true;
state.emit(" where ");
@ -1176,6 +1182,29 @@ ToStringResult toStringDetailed(TypeId ty, ToStringOptions& opts)
semi = true;
}
std::vector<std::pair<TypePackId, std::string>> sortedCycleTpNames(state.cycleTpNames.begin(), state.cycleTpNames.end());
std::sort(sortedCycleTpNames.begin(), sortedCycleTpNames.end(), [](const auto& a, const auto& b) {
return a.second < b.second;
});
TypePackStringifier tps{state};
for (const auto& [cycleTp, name] : sortedCycleTpNames)
{
if (semi)
state.emit(" ; ");
state.emit(name);
state.emit(" = ");
Luau::visit(
[&tps, cycleTy = cycleTp](auto&& t) {
return tps(cycleTy, t);
},
cycleTp->ty);
semi = true;
}
if (opts.maxTypeLength > 0 && result.name.length() > opts.maxTypeLength)
{
result.truncated = true;
@ -1361,22 +1390,30 @@ std::string toStringNamedFunction(const std::string& funcName, const FunctionTyp
return result.name;
}
static ToStringOptions& dumpOptions()
{
static ToStringOptions opts = ([]() {
ToStringOptions o;
o.exhaustive = true;
o.functionTypeArguments = true;
o.maxTableLength = 0;
o.maxTypeLength = 0;
return o;
})();
return opts;
}
std::string dump(TypeId ty)
{
ToStringOptions opts;
opts.exhaustive = true;
opts.functionTypeArguments = true;
std::string s = toString(ty, opts);
std::string s = toString(ty, dumpOptions());
printf("%s\n", s.c_str());
return s;
}
std::string dump(TypePackId ty)
{
ToStringOptions opts;
opts.exhaustive = true;
opts.functionTypeArguments = true;
std::string s = toString(ty, opts);
std::string s = toString(ty, dumpOptions());
printf("%s\n", s.c_str());
return s;
}
@ -1391,10 +1428,7 @@ std::string dump(const ScopePtr& scope, const char* name)
}
TypeId ty = binding->typeId;
ToStringOptions opts;
opts.exhaustive = true;
opts.functionTypeArguments = true;
std::string s = toString(ty, opts);
std::string s = toString(ty, dumpOptions());
printf("%s\n", s.c_str());
return s;
}
@ -1413,8 +1447,7 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts)
auto go = [&opts](auto&& c) -> std::string {
using T = std::decay_t<decltype(c)>;
auto tos = [&opts](auto&& a)
{
auto tos = [&opts](auto&& a) {
return toString(a, opts);
};
@ -1480,8 +1513,7 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts)
}
else if constexpr (std::is_same_v<T, PrimitiveTypeConstraint>)
{
return tos(c.resultType) + " ~ prim " + tos(c.expectedType) + ", " + tos(c.singletonType) + ", " +
tos(c.multitonType);
return tos(c.resultType) + " ~ prim " + tos(c.expectedType) + ", " + tos(c.singletonType) + ", " + tos(c.multitonType);
}
else if constexpr (std::is_same_v<T, HasPropConstraint>)
{
@ -1497,7 +1529,10 @@ std::string toString(const Constraint& constraint, ToStringOptions& opts)
std::string result = tos(c.resultType);
std::string discriminant = tos(c.discriminantType);
return result + " ~ if isSingleton D then ~D else unknown where D = " + discriminant;
if (c.negated)
return result + " ~ if isSingleton D then ~D else unknown where D = " + discriminant;
else
return result + " ~ if isSingleton D then D else unknown where D = " + discriminant;
}
else
static_assert(always_false_v<T>, "Non-exhaustive constraint switch");
@ -1516,28 +1551,8 @@ std::string dump(const Constraint& c)
return s;
}
std::string toString(const LValue& lvalue)
{
LUAU_ASSERT(!FFlag::LuauLvaluelessPath);
std::string s;
for (const LValue* current = &lvalue; current; current = baseof(*current))
{
if (auto field = get<Field>(*current))
s = "." + field->key + s;
else if (auto symbol = get<Symbol>(*current))
s = toString(*symbol) + s;
else
LUAU_ASSERT(!"Unknown LValue");
}
return s;
}
std::optional<std::string> getFunctionNameAsString(const AstExpr& expr)
{
LUAU_ASSERT(FFlag::LuauLvaluelessPath);
const AstExpr* curr = &expr;
std::string s;

View File

@ -150,7 +150,7 @@ Identifier mkName(const AstStatFunction& function)
auto name = mkName(*function.name);
LUAU_ASSERT(bool(name));
if (!name)
throwRuntimeError("Internal error: Function declaration has a bad name");
throw InternalCompilerError("Internal error: Function declaration has a bad name");
return *name;
}
@ -256,7 +256,7 @@ struct ArcCollector : public AstVisitor
{
auto name = mkName(*node->name);
if (!name)
throwRuntimeError("Internal error: AstStatFunction has a bad name");
throw InternalCompilerError("Internal error: AstStatFunction has a bad name");
add(*name);
return true;

View File

@ -78,6 +78,42 @@ void TxnLog::concat(TxnLog rhs)
typePackChanges[tp] = std::move(rep);
}
void TxnLog::concatAsIntersections(TxnLog rhs, NotNull<TypeArena> arena)
{
for (auto& [ty, rightRep] : rhs.typeVarChanges)
{
if (auto leftRep = typeVarChanges.find(ty))
{
TypeId leftTy = arena->addType((*leftRep)->pending);
TypeId rightTy = arena->addType(rightRep->pending);
typeVarChanges[ty]->pending.ty = IntersectionTypeVar{{leftTy, rightTy}};
}
else
typeVarChanges[ty] = std::move(rightRep);
}
for (auto& [tp, rep] : rhs.typePackChanges)
typePackChanges[tp] = std::move(rep);
}
void TxnLog::concatAsUnion(TxnLog rhs, NotNull<TypeArena> arena)
{
for (auto& [ty, rightRep] : rhs.typeVarChanges)
{
if (auto leftRep = typeVarChanges.find(ty))
{
TypeId leftTy = arena->addType((*leftRep)->pending);
TypeId rightTy = arena->addType(rightRep->pending);
typeVarChanges[ty]->pending.ty = UnionTypeVar{{leftTy, rightTy}};
}
else
typeVarChanges[ty] = std::move(rightRep);
}
for (auto& [tp, rep] : rhs.typePackChanges)
typePackChanges[tp] = std::move(rep);
}
void TxnLog::commit()
{
for (auto& [ty, rep] : typeVarChanges)

View File

@ -341,7 +341,7 @@ public:
AstType* operator()(const NegationTypeVar& ntv)
{
// FIXME: do the same thing we do with ErrorTypeVar
throwRuntimeError("Cannot convert NegationTypeVar into AstNode");
throw InternalCompilerError("Cannot convert NegationTypeVar into AstNode");
}
private:

View File

@ -295,11 +295,11 @@ struct TypeChecker2
Scope* scope = findInnermostScope(ret->location);
TypePackId expectedRetType = scope->returnType;
TypeArena arena;
TypePackId actualRetType = reconstructPack(ret->list, arena);
TypeArena* arena = &module->internalTypes;
TypePackId actualRetType = reconstructPack(ret->list, *arena);
UnifierSharedState sharedState{&ice};
Normalizer normalizer{&arena, singletonTypes, NotNull{&sharedState}};
Normalizer normalizer{arena, singletonTypes, NotNull{&sharedState}};
Unifier u{NotNull{&normalizer}, Mode::Strict, stack.back(), ret->location, Covariant};
u.tryUnify(actualRetType, expectedRetType);
@ -424,13 +424,13 @@ struct TypeChecker2
TypePackId iteratorPack = arena.addTypePack(valueTypes, iteratorTail);
// ... and then expand it out to 3 values (if possible)
const std::vector<TypeId> iteratorTypes = flatten(arena, singletonTypes, iteratorPack, 3);
if (iteratorTypes.empty())
TypePack iteratorTypes = extendTypePack(arena, singletonTypes, iteratorPack, 3);
if (iteratorTypes.head.empty())
{
reportError(GenericError{"for..in loops require at least one value to iterate over. Got zero"}, getLocation(forInStatement->values));
return;
}
TypeId iteratorTy = follow(iteratorTypes[0]);
TypeId iteratorTy = follow(iteratorTypes.head[0]);
auto checkFunction = [this, &arena, &scope, &forInStatement, &variableTypes](
const FunctionTypeVar* iterFtv, std::vector<TypeId> iterTys, bool isMm) {
@ -445,8 +445,8 @@ struct TypeChecker2
}
// It is okay if there aren't enough iterators, but the iteratee must provide enough.
std::vector<TypeId> expectedVariableTypes = flatten(arena, singletonTypes, iterFtv->retTypes, variableTypes.size());
if (expectedVariableTypes.size() < variableTypes.size())
TypePack expectedVariableTypes = extendTypePack(arena, singletonTypes, iterFtv->retTypes, variableTypes.size());
if (expectedVariableTypes.head.size() < variableTypes.size())
{
if (isMm)
reportError(
@ -455,8 +455,8 @@ struct TypeChecker2
reportError(GenericError{"next() does not return enough values"}, forInStatement->values.data[0]->location);
}
for (size_t i = 0; i < std::min(expectedVariableTypes.size(), variableTypes.size()); ++i)
reportErrors(tryUnify(scope, forInStatement->vars.data[i]->location, variableTypes[i], expectedVariableTypes[i]));
for (size_t i = 0; i < std::min(expectedVariableTypes.head.size(), variableTypes.size()); ++i)
reportErrors(tryUnify(scope, forInStatement->vars.data[i]->location, variableTypes[i], expectedVariableTypes.head[i]));
// nextFn is going to be invoked with (arrayTy, startIndexTy)
@ -477,25 +477,25 @@ struct TypeChecker2
if (maxCount && *maxCount < 2)
reportError(CountMismatch{2, std::nullopt, *maxCount, CountMismatch::Arg}, forInStatement->vars.data[0]->location);
const std::vector<TypeId> flattenedArgTypes = flatten(arena, singletonTypes, iterFtv->argTypes, 2);
TypePack flattenedArgTypes = extendTypePack(arena, singletonTypes, iterFtv->argTypes, 2);
size_t firstIterationArgCount = iterTys.empty() ? 0 : iterTys.size() - 1;
size_t actualArgCount = expectedVariableTypes.size();
size_t actualArgCount = expectedVariableTypes.head.size();
if (firstIterationArgCount < minCount)
reportError(CountMismatch{2, std::nullopt, firstIterationArgCount, CountMismatch::Arg}, forInStatement->vars.data[0]->location);
else if (actualArgCount < minCount)
reportError(CountMismatch{2, std::nullopt, actualArgCount, CountMismatch::Arg}, forInStatement->vars.data[0]->location);
if (iterTys.size() >= 2 && flattenedArgTypes.size() > 0)
if (iterTys.size() >= 2 && flattenedArgTypes.head.size() > 0)
{
size_t valueIndex = forInStatement->values.size > 1 ? 1 : 0;
reportErrors(tryUnify(scope, forInStatement->values.data[valueIndex]->location, iterTys[1], flattenedArgTypes[0]));
reportErrors(tryUnify(scope, forInStatement->values.data[valueIndex]->location, iterTys[1], flattenedArgTypes.head[0]));
}
if (iterTys.size() == 3 && flattenedArgTypes.size() > 1)
if (iterTys.size() == 3 && flattenedArgTypes.head.size() > 1)
{
size_t valueIndex = forInStatement->values.size > 2 ? 2 : 0;
reportErrors(tryUnify(scope, forInStatement->values.data[valueIndex]->location, iterTys[2], flattenedArgTypes[1]));
reportErrors(tryUnify(scope, forInStatement->values.data[valueIndex]->location, iterTys[2], flattenedArgTypes.head[1]));
}
};
@ -516,7 +516,7 @@ struct TypeChecker2
*/
if (const FunctionTypeVar* nextFn = get<FunctionTypeVar>(iteratorTy))
{
checkFunction(nextFn, iteratorTypes, false);
checkFunction(nextFn, iteratorTypes.head, false);
}
else if (const TableTypeVar* ttv = get<TableTypeVar>(iteratorTy))
{
@ -545,19 +545,19 @@ struct TypeChecker2
TypePackId argPack = arena.addTypePack({iteratorTy});
reportErrors(tryUnify(scope, forInStatement->values.data[0]->location, argPack, iterMmFtv->argTypes));
std::vector<TypeId> mmIteratorTypes = flatten(arena, singletonTypes, iterMmFtv->retTypes, 3);
TypePack mmIteratorTypes = extendTypePack(arena, singletonTypes, iterMmFtv->retTypes, 3);
if (mmIteratorTypes.size() == 0)
if (mmIteratorTypes.head.size() == 0)
{
reportError(GenericError{"__iter must return at least one value"}, forInStatement->values.data[0]->location);
return;
}
TypeId nextFn = follow(mmIteratorTypes[0]);
TypeId nextFn = follow(mmIteratorTypes.head[0]);
if (std::optional<TypeId> instantiatedNextFn = instantiation.substitute(nextFn))
{
std::vector<TypeId> instantiatedIteratorTypes = mmIteratorTypes;
std::vector<TypeId> instantiatedIteratorTypes = mmIteratorTypes.head;
instantiatedIteratorTypes[0] = *instantiatedNextFn;
if (const FunctionTypeVar* nextFtv = get<FunctionTypeVar>(*instantiatedNextFn))
@ -800,8 +800,8 @@ struct TypeChecker2
for (AstExpr* arg : call->args)
visit(arg);
TypeArena arena;
Instantiation instantiation{TxnLog::empty(), &arena, TypeLevel{}, stack.back()};
TypeArena* arena = &module->internalTypes;
Instantiation instantiation{TxnLog::empty(), arena, TypeLevel{}, stack.back()};
TypePackId expectedRetType = lookupPack(call);
TypeId functionType = lookupType(call->func);
@ -845,30 +845,70 @@ struct TypeChecker2
return;
}
}
else if (auto utv = get<UnionTypeVar>(functionType))
{
// Sometimes it's okay to call a union of functions, but only if all of the functions are the same.
std::optional<TypeId> fst;
for (TypeId ty : utv)
{
if (!fst)
fst = follow(ty);
else if (fst != follow(ty))
{
reportError(CannotCallNonFunction{functionType}, call->func->location);
return;
}
}
if (!fst)
ice.ice("UnionTypeVar had no elements, so fst is nullopt?");
if (std::optional<TypeId> instantiatedFunctionType = instantiation.substitute(*fst))
{
testFunctionType = *instantiatedFunctionType;
}
else
{
reportError(UnificationTooComplex{}, call->func->location);
return;
}
}
else
{
reportError(CannotCallNonFunction{functionType}, call->func->location);
return;
}
for (AstExpr* arg : call->args)
{
TypeId argTy = lookupType(arg);
args.head.push_back(argTy);
}
if (call->self)
{
AstExprIndexName* indexExpr = call->func->as<AstExprIndexName>();
if (!indexExpr)
ice.ice("method call expression has no 'self'");
args.head.insert(args.head.begin(), lookupType(indexExpr->expr));
args.head.push_back(lookupType(indexExpr->expr));
}
TypePackId argsTp = arena.addTypePack(args);
for (size_t i = 0; i < call->args.size; ++i)
{
AstExpr* arg = call->args.data[i];
TypeId* argTy = module->astTypes.find(arg);
if (argTy)
args.head.push_back(*argTy);
else if (i == call->args.size - 1)
{
TypePackId* argTail = module->astTypePacks.find(arg);
if (argTail)
args.tail = *argTail;
else
args.tail = singletonTypes->anyTypePack;
}
else
args.head.push_back(singletonTypes->anyType);
}
TypePackId argsTp = arena->addTypePack(args);
FunctionTypeVar ftv{argsTp, expectedRetType};
TypeId expectedType = arena.addType(ftv);
TypeId expectedType = arena->addType(ftv);
if (!isSubtype(testFunctionType, expectedType, stack.back()))
{
@ -881,19 +921,7 @@ struct TypeChecker2
void visit(AstExprIndexName* indexName)
{
TypeId leftType = lookupType(indexName->expr);
TypeId resultType = lookupType(indexName);
// leftType must have a property called indexName->index
std::optional<TypeId> ty =
getIndexTypeFromType(module->getModuleScope(), leftType, indexName->index.value, indexName->location, /* addErrors */ true);
if (ty)
{
if (!isSubtype(resultType, *ty, stack.back()))
{
reportError(TypeMismatch{resultType, *ty}, indexName->location);
}
}
getIndexTypeFromType(module->getModuleScope(), leftType, indexName->index.value, indexName->location, /* addErrors */ true);
}
void visit(AstExprIndexExpr* indexExpr)
@ -1085,7 +1113,7 @@ struct TypeChecker2
if (mm)
{
if (const FunctionTypeVar* ftv = get<FunctionTypeVar>(*mm))
if (const FunctionTypeVar* ftv = get<FunctionTypeVar>(follow(*mm)))
{
TypePackId expectedArgs;
// For >= and > we invoke __lt and __le respectively with

View File

@ -35,11 +35,12 @@ LUAU_FASTFLAG(LuauTypeNormalization2)
LUAU_FASTFLAGVARIABLE(DebugLuauFreezeDuringUnification, false)
LUAU_FASTFLAGVARIABLE(LuauReturnAnyInsteadOfICE, false) // Eventually removed as false.
LUAU_FASTFLAGVARIABLE(DebugLuauSharedSelf, false)
LUAU_FASTFLAGVARIABLE(LuauLvaluelessPath, false)
LUAU_FASTFLAGVARIABLE(LuauNilIterator, false)
LUAU_FASTFLAGVARIABLE(LuauUnknownAndNeverType, false)
LUAU_FASTFLAGVARIABLE(LuauTypeInferMissingFollows, false)
LUAU_FASTFLAGVARIABLE(LuauBinaryNeedsExpectedTypesToo, false)
LUAU_FASTFLAGVARIABLE(LuauNeverTypesAndOperatorsInference, false)
LUAU_FASTFLAGVARIABLE(LuauFollowInLvalueIndexCheck, false)
LUAU_FASTFLAGVARIABLE(LuauReturnsFromCallsitesAreNotWidened, false)
LUAU_FASTFLAGVARIABLE(LuauTryhardAnd, false)
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
@ -47,18 +48,15 @@ LUAU_FASTFLAGVARIABLE(LuauCompleteVisitor, false)
LUAU_FASTFLAGVARIABLE(LuauOptionalNextKey, false)
LUAU_FASTFLAGVARIABLE(LuauReportShadowedTypeAlias, false)
LUAU_FASTFLAGVARIABLE(LuauBetterMessagingOnCountMismatch, false)
LUAU_FASTFLAGVARIABLE(LuauArgMismatchReportFunctionLocation, false)
LUAU_FASTFLAGVARIABLE(LuauIntersectionTestForEquality, false)
LUAU_FASTFLAGVARIABLE(LuauImplicitElseRefinement, false)
LUAU_FASTFLAGVARIABLE(LuauAllowIndexClassParameters, false)
LUAU_FASTFLAGVARIABLE(LuauDeclareClassPrototype, false)
LUAU_FASTFLAG(LuauUninhabitedSubAnything)
LUAU_FASTFLAGVARIABLE(LuauCallableClasses, false)
namespace Luau
{
const char* TimeLimitError_DEPRECATED::what() const throw()
{
LUAU_ASSERT(!FFlag::LuauIceExceptionInheritanceChange);
return "Typeinfer failed to complete in allotted time";
}
static bool typeCouldHaveMetatable(TypeId ty)
{
@ -269,11 +267,6 @@ ModulePtr TypeChecker::check(const SourceModule& module, Mode mode, std::optiona
reportErrorCodeTooComplex(module.root->location);
return std::move(currentModule);
}
catch (const RecursionLimitException_DEPRECATED&)
{
reportErrorCodeTooComplex(module.root->location);
return std::move(currentModule);
}
}
ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mode mode, std::optional<ScopePtr> environmentScope)
@ -318,10 +311,6 @@ ModulePtr TypeChecker::checkWithoutRecursionCheck(const SourceModule& module, Mo
{
currentModule->timeout = true;
}
catch (const TimeLimitError_DEPRECATED&)
{
currentModule->timeout = true;
}
if (FFlag::DebugLuauSharedSelf)
{
@ -429,7 +418,7 @@ void TypeChecker::check(const ScopePtr& scope, const AstStat& program)
ice("Unknown AstStat");
if (finishTime && TimeTrace::getClock() > *finishTime)
throwTimeLimitError();
throw TimeLimitError(iceHandler->moduleName);
}
// This particular overload is for do...end. If you need to not increase the scope level, use checkBlock directly.
@ -456,11 +445,6 @@ void TypeChecker::checkBlock(const ScopePtr& scope, const AstStatBlock& block)
reportErrorCodeTooComplex(block.location);
return;
}
catch (const RecursionLimitException_DEPRECATED&)
{
reportErrorCodeTooComplex(block.location);
return;
}
}
struct InplaceDemoter : TypeVarOnceVisitor
@ -966,9 +950,9 @@ void TypeChecker::check(const ScopePtr& scope, const AstStatAssign& assign)
TypeId right = nullptr;
Location loc = 0 == assign.values.size ? assign.location
: i < assign.values.size ? assign.values.data[i]->location
: assign.values.data[assign.values.size - 1]->location;
Location loc = 0 == assign.values.size
? assign.location
: i < assign.values.size ? assign.values.data[i]->location : assign.values.data[assign.values.size - 1]->location;
if (valueIter != valueEnd)
{
@ -2671,6 +2655,48 @@ static std::optional<std::string> getIdentifierOfBaseVar(AstExpr* node)
return std::nullopt;
}
/** Return true if comparison between the types a and b should be permitted with
* the == or ~= operators.
*
* Two types are considered eligible for equality testing if it is possible for
* the test to ever succeed. In other words, we test to see whether the two
* types have any overlap at all.
*
* In order to make things work smoothly with the greedy solver, this function
* exempts any and FreeTypeVars from this requirement.
*
* This function does not (yet?) take into account extra Lua restrictions like
* that two tables can only be compared if they have the same metatable. That
* is presently handled by the caller.
*
* @return True if the types are comparable. False if they are not.
*
* If an internal recursion limit is reached while performing this test, the
* function returns std::nullopt.
*/
static std::optional<bool> areEqComparable(NotNull<TypeArena> arena, NotNull<Normalizer> normalizer, TypeId a, TypeId b)
{
a = follow(a);
b = follow(b);
auto isExempt = [](TypeId t) {
return isNil(t) || get<FreeTypeVar>(t);
};
if (isExempt(a) || isExempt(b))
return true;
TypeId c = arena->addType(IntersectionTypeVar{{a, b}});
const NormalizedType* n = normalizer->normalize(c);
if (!n)
return std::nullopt;
if (FFlag::LuauUninhabitedSubAnything)
return normalizer->isInhabited(n);
else
return isInhabited_DEPRECATED(*n);
}
TypeId TypeChecker::checkRelationalOperation(
const ScopePtr& scope, const AstExprBinary& expr, TypeId lhsType, TypeId rhsType, const PredicateVec& predicates)
{
@ -2741,6 +2767,28 @@ TypeId TypeChecker::checkRelationalOperation(
return booleanType;
}
if (FFlag::LuauIntersectionTestForEquality && isEquality)
{
// Unless either type is free or any, an equality comparison is only
// valid when the intersection of the two operands is non-empty.
//
// eg it is okay to compare string? == number? because the two types
// have nil in common, but string == number is not allowed.
std::optional<bool> eqTestResult = areEqComparable(NotNull{&currentModule->internalTypes}, NotNull{&normalizer}, lhsType, rhsType);
if (!eqTestResult)
{
reportErrorCodeTooComplex(expr.location);
return errorRecoveryType(booleanType);
}
if (!*eqTestResult)
{
reportError(
expr.location, GenericError{format("Type %s cannot be compared with %s", toString(lhsType).c_str(), toString(rhsType).c_str())});
return errorRecoveryType(booleanType);
}
}
/* Subtlety here:
* We need to do this unification first, but there are situations where we don't actually want to
* report any problems that might have been surfaced as a result of this step because we might already
@ -2753,7 +2801,7 @@ TypeId TypeChecker::checkRelationalOperation(
state.log.commit();
}
bool needsMetamethod = !isEquality;
const bool needsMetamethod = !isEquality;
TypeId leftType = follow(lhsType);
if (get<PrimitiveTypeVar>(leftType) || get<AnyTypeVar>(leftType) || get<ErrorTypeVar>(leftType) || get<UnionTypeVar>(leftType))
@ -3335,6 +3383,9 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
TypeId indexType = checkExpr(scope, *expr.index).type;
if (FFlag::LuauFollowInLvalueIndexCheck)
exprType = follow(exprType);
if (get<AnyTypeVar>(exprType) || get<ErrorTypeVar>(exprType))
return exprType;
@ -3356,6 +3407,16 @@ TypeId TypeChecker::checkLValueBinding(const ScopePtr& scope, const AstExprIndex
return prop->type;
}
}
else if (FFlag::LuauAllowIndexClassParameters)
{
if (const ClassTypeVar* exprClass = get<ClassTypeVar>(exprType))
{
if (isNonstrictMode())
return unknownType;
reportError(TypeError{expr.location, DynamicPropertyLookupOnClassesUnsafe{exprType}});
return errorRecoveryType(scope);
}
}
TableTypeVar* exprTable = getMutableTableType(exprType);
@ -3810,16 +3871,8 @@ void TypeChecker::checkArgumentList(const ScopePtr& scope, const AstExpr& funNam
std::string namePath;
if (FFlag::LuauLvaluelessPath)
{
if (std::optional<std::string> path = getFunctionNameAsString(funName))
namePath = *path;
}
else
{
if (std::optional<LValue> lValue = tryGetLValue(funName))
namePath = toString(*lValue);
}
if (std::optional<std::string> path = getFunctionNameAsString(funName))
namePath = *path;
auto [minParams, optMaxParams] = getParameterExtents(&state.log, paramPack);
state.reportError(TypeError{location,
@ -3929,27 +3982,11 @@ void TypeChecker::checkArgumentList(const ScopePtr& scope, const AstExpr& funNam
std::string namePath;
if (FFlag::LuauLvaluelessPath)
{
if (std::optional<std::string> path = getFunctionNameAsString(funName))
namePath = *path;
}
else
{
if (std::optional<LValue> lValue = tryGetLValue(funName))
namePath = toString(*lValue);
}
if (std::optional<std::string> path = getFunctionNameAsString(funName))
namePath = *path;
if (FFlag::LuauArgMismatchReportFunctionLocation)
{
state.reportError(TypeError{
funName.location, CountMismatch{minParams, optMaxParams, paramIndex, CountMismatch::Context::Arg, isVariadic, namePath}});
}
else
{
state.reportError(TypeError{
state.location, CountMismatch{minParams, optMaxParams, paramIndex, CountMismatch::Context::Arg, isVariadic, namePath}});
}
state.reportError(TypeError{
funName.location, CountMismatch{minParams, optMaxParams, paramIndex, CountMismatch::Context::Arg, isVariadic, namePath}});
return;
}
++paramIter;
@ -4461,7 +4498,7 @@ void TypeChecker::reportOverloadResolutionError(const ScopePtr& scope, const Ast
std::string s;
for (size_t i = 0; i < overloadTypes.size(); ++i)
{
TypeId overload = overloadTypes[i];
TypeId overload = FFlag::LuauTypeInferMissingFollows ? follow(overloadTypes[i]) : overloadTypes[i];
Unifier state = mkUnifier(scope, expr.location);
// Unify return types
@ -4842,7 +4879,10 @@ TypePackId TypeChecker::anyifyModuleReturnTypePackGenerics(TypePackId tp)
tp = follow(tp);
if (const VariadicTypePack* vtp = get<VariadicTypePack>(tp))
return get<GenericTypeVar>(vtp->ty) ? anyTypePack : tp;
{
TypeId ty = FFlag::LuauTypeInferMissingFollows ? follow(vtp->ty) : vtp->ty;
return get<GenericTypeVar>(ty) ? anyTypePack : tp;
}
if (!get<TypePack>(follow(tp)))
return tp;
@ -4893,19 +4933,6 @@ void TypeChecker::ice(const std::string& message)
iceHandler->ice(message);
}
// TODO: Inline me when LuauIceExceptionInheritanceChange is deleted.
void TypeChecker::throwTimeLimitError()
{
if (FFlag::LuauIceExceptionInheritanceChange)
{
throw TimeLimitError(iceHandler->moduleName);
}
else
{
throw TimeLimitError_DEPRECATED();
}
}
void TypeChecker::prepareErrorsForDisplay(ErrorVec& errVec)
{
// Remove errors with names that were generated by recovery from a parse error
@ -6085,11 +6112,11 @@ void TypeChecker::resolve(const EqPredicate& eqP, RefinementMap& refis, const Sc
if (optionIsSubtype && !targetIsSubtype)
return option;
else if (!optionIsSubtype && targetIsSubtype)
return eqP.type;
return FFlag::LuauTypeInferMissingFollows ? follow(eqP.type) : eqP.type;
else if (!optionIsSubtype && !targetIsSubtype)
return nope;
else if (optionIsSubtype && targetIsSubtype)
return eqP.type;
return FFlag::LuauTypeInferMissingFollows ? follow(eqP.type) : eqP.type;
}
else
{

View File

@ -6,6 +6,8 @@
#include <stdexcept>
LUAU_FASTFLAGVARIABLE(LuauTxnLogTypePackIterator, false)
namespace Luau
{
@ -60,8 +62,8 @@ TypePackIterator::TypePackIterator(TypePackId typePack)
}
TypePackIterator::TypePackIterator(TypePackId typePack, const TxnLog* log)
: currentTypePack(follow(typePack))
, tp(get<TypePack>(currentTypePack))
: currentTypePack(FFlag::LuauTxnLogTypePackIterator ? log->follow(typePack) : follow(typePack))
, tp(FFlag::LuauTxnLogTypePackIterator ? log->get<TypePack>(currentTypePack) : get<TypePack>(currentTypePack))
, currentIndex(0)
, log(log)
{
@ -235,7 +237,7 @@ TypePackId follow(TypePackId tp, std::function<TypePackId(TypePackId)> mapper)
cycleTester = nullptr;
if (tp == cycleTester)
throwRuntimeError("Luau::follow detected a TypeVar cycle!!");
throw InternalCompilerError("Luau::follow detected a TypeVar cycle!!");
}
}
}

View File

@ -122,10 +122,6 @@ std::optional<TypeId> getIndexTypeFromType(const ScopePtr& scope, ErrorVec& erro
for (TypeId t : utv)
{
// TODO: we should probably limit recursion here?
// RecursionLimiter _rl(&recursionCount, FInt::LuauTypeInferRecursionLimit);
// Not needed when we normalize types.
if (get<AnyTypeVar>(follow(t)))
return t;
@ -164,9 +160,6 @@ std::optional<TypeId> getIndexTypeFromType(const ScopePtr& scope, ErrorVec& erro
for (TypeId t : itv->parts)
{
// TODO: we should probably limit recursion here?
// RecursionLimiter _rl(&recursionCount, FInt::LuauTypeInferRecursionLimit);
if (std::optional<TypeId> ty =
getIndexTypeFromType(scope, errors, arena, singletonTypes, t, prop, location, /* addErrors= */ false, handle))
parts.push_back(*ty);
@ -183,7 +176,7 @@ std::optional<TypeId> getIndexTypeFromType(const ScopePtr& scope, ErrorVec& erro
if (parts.size() == 1)
return parts[0];
return arena->addType(IntersectionTypeVar{std::move(parts)}); // Not at all correct.
return arena->addType(IntersectionTypeVar{std::move(parts)});
}
if (addErrors)
@ -221,46 +214,95 @@ std::pair<size_t, std::optional<size_t>> getParameterExtents(const TxnLog* log,
return {minCount, minCount + optionalCount};
}
std::vector<TypeId> flatten(TypeArena& arena, NotNull<SingletonTypes> singletonTypes, TypePackId pack, size_t length)
TypePack extendTypePack(TypeArena& arena, NotNull<SingletonTypes> singletonTypes, TypePackId pack, size_t length)
{
std::vector<TypeId> result;
TypePack result;
auto it = begin(pack);
auto endIt = end(pack);
while (it != endIt)
while (true)
{
result.push_back(*it);
pack = follow(pack);
if (result.size() >= length)
if (const TypePack* p = get<TypePack>(pack))
{
size_t i = 0;
while (i < p->head.size() && result.head.size() < length)
{
result.head.push_back(p->head[i]);
++i;
}
if (result.head.size() == length)
{
if (i == p->head.size())
result.tail = p->tail;
else
{
TypePackId newTail = arena.addTypePack(TypePack{});
TypePack* newTailPack = getMutable<TypePack>(newTail);
newTailPack->head.insert(newTailPack->head.begin(), p->head.begin() + i, p->head.end());
newTailPack->tail = p->tail;
result.tail = newTail;
}
return result;
}
else if (p->tail)
{
pack = *p->tail;
continue;
}
else
{
// There just aren't enough types in this pack to satisfy the request.
return result;
}
}
else if (const VariadicTypePack* vtp = get<VariadicTypePack>(pack))
{
while (result.head.size() < length)
result.head.push_back(vtp->ty);
result.tail = pack;
return result;
}
else if (FreeTypePack* ftp = getMutable<FreeTypePack>(pack))
{
// If we need to get concrete types out of a free pack, we choose to
// interpret this as proof that the pack must have at least 'length'
// elements. We mint fresh types for each element we're extracting
// and rebind the free pack to be a TypePack containing them. We
// also have to create a new tail.
++it;
}
TypePack newPack;
newPack.tail = arena.freshTypePack(ftp->scope);
if (!it.tail())
return result;
while (result.head.size() < length)
{
newPack.head.push_back(arena.freshType(ftp->scope));
result.head.push_back(newPack.head.back());
}
TypePackId tail = *it.tail();
if (get<TypePack>(tail))
LUAU_ASSERT(0);
else if (auto vtp = get<VariadicTypePack>(tail))
{
while (result.size() < length)
result.push_back(vtp->ty);
}
else if (get<FreeTypePack>(tail) || get<GenericTypePack>(tail))
{
while (result.size() < length)
result.push_back(arena.addType(FreeTypeVar{nullptr}));
}
else if (auto etp = get<Unifiable::Error>(tail))
{
while (result.size() < length)
result.push_back(singletonTypes->errorRecoveryType());
}
asMutable(pack)->ty.emplace<TypePack>(std::move(newPack));
return result;
return result;
}
else if (const Unifiable::Error* etp = getMutable<Unifiable::Error>(pack))
{
while (result.head.size() < length)
result.head.push_back(singletonTypes->errorRecoveryType());
result.tail = pack;
return result;
}
else
{
// If the pack is blocked or generic, we can't extract.
// Return whatever we've got with this pack as the tail.
result.tail = pack;
return result;
}
}
}
std::vector<TypeId> reduceUnion(const std::vector<TypeId>& types)

View File

@ -72,7 +72,7 @@ TypeId follow(TypeId t, std::function<TypeId(TypeId)> mapper)
{
TypeId res = ltv->thunk();
if (get<LazyTypeVar>(res))
throwRuntimeError("Lazy TypeVar cannot resolve to another Lazy TypeVar");
throw InternalCompilerError("Lazy TypeVar cannot resolve to another Lazy TypeVar");
*asMutable(ty) = BoundTypeVar(res);
}
@ -110,7 +110,7 @@ TypeId follow(TypeId t, std::function<TypeId(TypeId)> mapper)
cycleTester = nullptr;
if (t == cycleTester)
throwRuntimeError("Luau::follow detected a TypeVar cycle!!");
throw InternalCompilerError("Luau::follow detected a TypeVar cycle!!");
}
}
}
@ -468,65 +468,65 @@ PendingExpansionTypeVar::PendingExpansionTypeVar(
size_t PendingExpansionTypeVar::nextIndex = 0;
FunctionTypeVar::FunctionTypeVar(TypePackId argTypes, TypePackId retTypes, std::optional<FunctionDefinition> defn, bool hasSelf)
: argTypes(argTypes)
: definition(std::move(defn))
, argTypes(argTypes)
, retTypes(retTypes)
, definition(std::move(defn))
, hasSelf(hasSelf)
{
}
FunctionTypeVar::FunctionTypeVar(TypeLevel level, TypePackId argTypes, TypePackId retTypes, std::optional<FunctionDefinition> defn, bool hasSelf)
: level(level)
: definition(std::move(defn))
, level(level)
, argTypes(argTypes)
, retTypes(retTypes)
, definition(std::move(defn))
, hasSelf(hasSelf)
{
}
FunctionTypeVar::FunctionTypeVar(
TypeLevel level, Scope* scope, TypePackId argTypes, TypePackId retTypes, std::optional<FunctionDefinition> defn, bool hasSelf)
: level(level)
: definition(std::move(defn))
, level(level)
, scope(scope)
, argTypes(argTypes)
, retTypes(retTypes)
, definition(std::move(defn))
, hasSelf(hasSelf)
{
}
FunctionTypeVar::FunctionTypeVar(std::vector<TypeId> generics, std::vector<TypePackId> genericPacks, TypePackId argTypes, TypePackId retTypes,
std::optional<FunctionDefinition> defn, bool hasSelf)
: generics(generics)
: definition(std::move(defn))
, generics(generics)
, genericPacks(genericPacks)
, argTypes(argTypes)
, retTypes(retTypes)
, definition(std::move(defn))
, hasSelf(hasSelf)
{
}
FunctionTypeVar::FunctionTypeVar(TypeLevel level, std::vector<TypeId> generics, std::vector<TypePackId> genericPacks, TypePackId argTypes,
TypePackId retTypes, std::optional<FunctionDefinition> defn, bool hasSelf)
: level(level)
: definition(std::move(defn))
, generics(generics)
, genericPacks(genericPacks)
, level(level)
, argTypes(argTypes)
, retTypes(retTypes)
, definition(std::move(defn))
, hasSelf(hasSelf)
{
}
FunctionTypeVar::FunctionTypeVar(TypeLevel level, Scope* scope, std::vector<TypeId> generics, std::vector<TypePackId> genericPacks,
TypePackId argTypes, TypePackId retTypes, std::optional<FunctionDefinition> defn, bool hasSelf)
: level(level)
, scope(scope)
: definition(std::move(defn))
, generics(generics)
, genericPacks(genericPacks)
, level(level)
, scope(scope)
, argTypes(argTypes)
, retTypes(retTypes)
, definition(std::move(defn))
, hasSelf(hasSelf)
{
}

View File

@ -48,6 +48,8 @@ void* pagedAllocate(size_t size)
// On Linux, we must use mmap because using regular heap results in mprotect() fragmenting the page table and us bumping into 64K mmap limit.
#ifdef _WIN32
return _aligned_malloc(size, kPageSize);
#elif defined(__FreeBSD__)
return aligned_alloc(kPageSize, size);
#else
return mmap(nullptr, pageAlign(size), PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANON, -1, 0);
#endif
@ -61,6 +63,8 @@ void pagedDeallocate(void* ptr, size_t size)
#ifdef _WIN32
_aligned_free(ptr);
#elif defined(__FreeBSD__)
free(ptr);
#else
int rc = munmap(ptr, size);
LUAU_ASSERT(rc == 0);

View File

@ -22,8 +22,10 @@ LUAU_FASTFLAGVARIABLE(LuauSubtypeNormalizer, false);
LUAU_FASTFLAGVARIABLE(LuauScalarShapeSubtyping, false)
LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping, false)
LUAU_FASTFLAGVARIABLE(LuauOverloadedFunctionSubtypingPerf, false);
LUAU_FASTFLAGVARIABLE(LuauScalarShapeUnifyToMtOwner, false)
LUAU_FASTFLAGVARIABLE(LuauScalarShapeUnifyToMtOwner2, false)
LUAU_FASTFLAGVARIABLE(LuauUninhabitedSubAnything, false)
LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution)
LUAU_FASTFLAG(LuauTxnLogTypePackIterator)
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(LuauNegatedFunctionTypes)
@ -51,6 +53,9 @@ struct PromoteTypeLevels final : TypeVarOnceVisitor
template<typename TID, typename T>
void promote(TID ty, T* t)
{
if (FFlag::DebugLuauDeferredConstraintResolution && !t)
return;
LUAU_ASSERT(t);
if (useScopes)
@ -102,6 +107,11 @@ struct PromoteTypeLevels final : TypeVarOnceVisitor
if (ty->owningArena != typeArena)
return false;
// Surprise, it's actually a BoundTypePack that hasn't been committed yet.
// Calling getMutable on this will trigger an assertion.
if (FFlag::LuauScalarShapeUnifyToMtOwner2 && !log.is<FunctionTypeVar>(ty))
return true;
promote(ty, log.getMutable<FunctionTypeVar>(ty));
return true;
}
@ -115,6 +125,11 @@ struct PromoteTypeLevels final : TypeVarOnceVisitor
if (ttv.state != TableState::Free && ttv.state != TableState::Generic)
return true;
// Surprise, it's actually a BoundTypePack that hasn't been committed yet.
// Calling getMutable on this will trigger an assertion.
if (FFlag::LuauScalarShapeUnifyToMtOwner2 && !log.is<TableTypeVar>(ty))
return true;
promote(ty, log.getMutable<TableTypeVar>(ty));
return true;
}
@ -277,7 +292,7 @@ TypeId Widen::clean(TypeId ty)
TypePackId Widen::clean(TypePackId)
{
throwRuntimeError("Widen attempted to clean a dirty type pack?");
throw InternalCompilerError("Widen attempted to clean a dirty type pack?");
}
bool Widen::ignoreChildren(TypeId ty)
@ -336,6 +351,20 @@ static bool subsumes(bool useScopes, TY_A* left, TY_B* right)
return left->level.subsumes(right->level);
}
TypeMismatch::Context Unifier::mismatchContext()
{
switch (variance)
{
case Covariant:
return TypeMismatch::CovariantContext;
case Invariant:
return TypeMismatch::InvariantContext;
default:
LUAU_ASSERT(false); // This codepath should be unreachable.
return TypeMismatch::CovariantContext;
}
}
Unifier::Unifier(NotNull<Normalizer> normalizer, Mode mode, NotNull<Scope> scope, const Location& location, Variance variance, TxnLog* parentLog)
: types(normalizer->arena)
, singletonTypes(normalizer->singletonTypes)
@ -559,8 +588,11 @@ void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool
else if (log.get<NegationTypeVar>(subTy))
tryUnifyNegationWithType(subTy, superTy);
else if (FFlag::LuauUninhabitedSubAnything && !normalizer->isInhabited(subTy))
{}
else
reportError(location, TypeMismatch{superTy, subTy});
reportError(location, TypeMismatch{superTy, subTy, mismatchContext()});
if (cacheEnabled)
cacheResult(subTy, superTy, errorCount);
@ -575,11 +607,16 @@ void Unifier::tryUnifyUnionWithType(TypeId subTy, const UnionTypeVar* subUnion,
std::optional<TypeError> unificationTooComplex;
std::optional<TypeError> firstFailedOption;
std::vector<TxnLog> logs;
for (TypeId type : subUnion->options)
{
Unifier innerState = makeChildUnifier();
innerState.tryUnify_(type, superTy);
if (FFlag::DebugLuauDeferredConstraintResolution)
logs.push_back(std::move(innerState.log));
if (auto e = hasUnificationTooComplex(innerState.errors))
unificationTooComplex = e;
else if (!innerState.errors.empty())
@ -592,51 +629,56 @@ void Unifier::tryUnifyUnionWithType(TypeId subTy, const UnionTypeVar* subUnion,
}
}
// even if A | B <: T fails, we want to bind some options of T with A | B iff A | B was a subtype of that option.
auto tryBind = [this, subTy](TypeId superOption) {
superOption = log.follow(superOption);
// just skip if the superOption is not free-ish.
auto ttv = log.getMutable<TableTypeVar>(superOption);
if (!log.is<FreeTypeVar>(superOption) && (!ttv || ttv->state != TableState::Free))
return;
// If superOption is already present in subTy, do nothing. Nothing new has been learned, but the subtype
// test is successful.
if (auto subUnion = get<UnionTypeVar>(subTy))
{
if (end(subUnion) != std::find(begin(subUnion), end(subUnion), superOption))
return;
}
// Since we have already checked if S <: T, checking it again will not queue up the type for replacement.
// So we'll have to do it ourselves. We assume they unified cleanly if they are still in the seen set.
if (log.haveSeen(subTy, superOption))
{
// TODO: would it be nice for TxnLog::replace to do this?
if (log.is<TableTypeVar>(superOption))
log.bindTable(superOption, subTy);
else
log.replace(superOption, *subTy);
}
};
if (auto superUnion = log.getMutable<UnionTypeVar>(superTy))
{
for (TypeId ty : superUnion)
tryBind(ty);
}
if (FFlag::DebugLuauDeferredConstraintResolution)
log.concatAsUnion(combineLogsIntoUnion(std::move(logs)), NotNull{types});
else
tryBind(superTy);
{
// even if A | B <: T fails, we want to bind some options of T with A | B iff A | B was a subtype of that option.
auto tryBind = [this, subTy](TypeId superOption) {
superOption = log.follow(superOption);
// just skip if the superOption is not free-ish.
auto ttv = log.getMutable<TableTypeVar>(superOption);
if (!log.is<FreeTypeVar>(superOption) && (!ttv || ttv->state != TableState::Free))
return;
// If superOption is already present in subTy, do nothing. Nothing new has been learned, but the subtype
// test is successful.
if (auto subUnion = get<UnionTypeVar>(subTy))
{
if (end(subUnion) != std::find(begin(subUnion), end(subUnion), superOption))
return;
}
// Since we have already checked if S <: T, checking it again will not queue up the type for replacement.
// So we'll have to do it ourselves. We assume they unified cleanly if they are still in the seen set.
if (log.haveSeen(subTy, superOption))
{
// TODO: would it be nice for TxnLog::replace to do this?
if (log.is<TableTypeVar>(superOption))
log.bindTable(superOption, subTy);
else
log.replace(superOption, *subTy);
}
};
if (auto superUnion = log.getMutable<UnionTypeVar>(superTy))
{
for (TypeId ty : superUnion)
tryBind(ty);
}
else
tryBind(superTy);
}
if (unificationTooComplex)
reportError(*unificationTooComplex);
else if (failed)
{
if (firstFailedOption)
reportError(location, TypeMismatch{superTy, subTy, "Not all union options are compatible.", *firstFailedOption});
reportError(location, TypeMismatch{superTy, subTy, "Not all union options are compatible.", *firstFailedOption, mismatchContext()});
else
reportError(location, TypeMismatch{superTy, subTy});
reportError(location, TypeMismatch{superTy, subTy, mismatchContext()});
}
}
@ -696,6 +738,8 @@ void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTyp
}
}
std::vector<TxnLog> logs;
for (size_t i = 0; i < uv->options.size(); ++i)
{
TypeId type = uv->options[(i + startIndex) % uv->options.size()];
@ -706,9 +750,13 @@ void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTyp
if (innerState.errors.empty())
{
found = true;
log.concat(std::move(innerState.log));
break;
if (FFlag::DebugLuauDeferredConstraintResolution)
logs.push_back(std::move(innerState.log));
else
{
log.concat(std::move(innerState.log));
break;
}
}
else if (auto e = hasUnificationTooComplex(innerState.errors))
{
@ -723,6 +771,9 @@ void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTyp
}
}
if (FFlag::DebugLuauDeferredConstraintResolution)
log.concatAsUnion(combineLogsIntoUnion(std::move(logs)), NotNull{types});
if (unificationTooComplex)
{
reportError(*unificationTooComplex);
@ -744,9 +795,10 @@ void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionTyp
else if (!found)
{
if ((failedOptionCount == 1 || foundHeuristic) && failedOption)
reportError(location, TypeMismatch{superTy, subTy, "None of the union options are compatible. For example:", *failedOption});
reportError(
location, TypeMismatch{superTy, subTy, "None of the union options are compatible. For example:", *failedOption, mismatchContext()});
else
reportError(location, TypeMismatch{superTy, subTy, "none of the union options are compatible"});
reportError(location, TypeMismatch{superTy, subTy, "none of the union options are compatible", mismatchContext()});
}
}
@ -755,6 +807,8 @@ void Unifier::tryUnifyTypeWithIntersection(TypeId subTy, TypeId superTy, const I
std::optional<TypeError> unificationTooComplex;
std::optional<TypeError> firstFailedOption;
std::vector<TxnLog> logs;
// T <: A & B if and only if T <: A and T <: B
for (TypeId type : uv->parts)
{
@ -769,13 +823,19 @@ void Unifier::tryUnifyTypeWithIntersection(TypeId subTy, TypeId superTy, const I
firstFailedOption = {innerState.errors.front()};
}
log.concat(std::move(innerState.log));
if (FFlag::DebugLuauDeferredConstraintResolution)
logs.push_back(std::move(innerState.log));
else
log.concat(std::move(innerState.log));
}
if (FFlag::DebugLuauDeferredConstraintResolution)
log.concat(combineLogsIntoIntersection(std::move(logs)));
if (unificationTooComplex)
reportError(*unificationTooComplex);
else if (firstFailedOption)
reportError(location, TypeMismatch{superTy, subTy, "Not all intersection parts are compatible.", *firstFailedOption});
reportError(location, TypeMismatch{superTy, subTy, "Not all intersection parts are compatible.", *firstFailedOption, mismatchContext()});
}
void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionTypeVar* uv, TypeId superTy, bool cacheEnabled, bool isFunctionCall)
@ -802,6 +862,8 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionTypeV
}
}
std::vector<TxnLog> logs;
for (size_t i = 0; i < uv->parts.size(); ++i)
{
TypeId type = uv->parts[(i + startIndex) % uv->parts.size()];
@ -812,8 +874,13 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionTypeV
if (innerState.errors.empty())
{
found = true;
log.concat(std::move(innerState.log));
break;
if (FFlag::DebugLuauDeferredConstraintResolution)
logs.push_back(std::move(innerState.log));
else
{
log.concat(std::move(innerState.log));
break;
}
}
else if (auto e = hasUnificationTooComplex(innerState.errors))
{
@ -821,6 +888,9 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionTypeV
}
}
if (FFlag::DebugLuauDeferredConstraintResolution)
log.concat(combineLogsIntoIntersection(std::move(logs)));
if (unificationTooComplex)
reportError(*unificationTooComplex);
else if (!found && normalize)
@ -837,7 +907,7 @@ void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionTypeV
}
else if (!found)
{
reportError(location, TypeMismatch{superTy, subTy, "none of the intersection parts are compatible"});
reportError(location, TypeMismatch{superTy, subTy, "none of the intersection parts are compatible", mismatchContext()});
}
}
@ -849,37 +919,37 @@ void Unifier::tryUnifyNormalizedTypes(
if (get<UnknownTypeVar>(superNorm.tops) || get<AnyTypeVar>(superNorm.tops) || get<AnyTypeVar>(subNorm.tops))
return;
else if (get<UnknownTypeVar>(subNorm.tops))
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
if (get<ErrorTypeVar>(subNorm.errors))
if (!get<ErrorTypeVar>(superNorm.errors))
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
if (get<PrimitiveTypeVar>(subNorm.booleans))
{
if (!get<PrimitiveTypeVar>(superNorm.booleans))
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
}
else if (const SingletonTypeVar* stv = get<SingletonTypeVar>(subNorm.booleans))
{
if (!get<PrimitiveTypeVar>(superNorm.booleans) && stv != get<SingletonTypeVar>(superNorm.booleans))
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
}
if (get<PrimitiveTypeVar>(subNorm.nils))
if (!get<PrimitiveTypeVar>(superNorm.nils))
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
if (get<PrimitiveTypeVar>(subNorm.numbers))
if (!get<PrimitiveTypeVar>(superNorm.numbers))
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
if (!isSubtype(subNorm.strings, superNorm.strings))
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
if (get<PrimitiveTypeVar>(subNorm.threads))
if (!get<PrimitiveTypeVar>(superNorm.errors))
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
for (TypeId subClass : subNorm.classes)
{
@ -895,7 +965,7 @@ void Unifier::tryUnifyNormalizedTypes(
}
}
if (!found)
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
}
for (TypeId subTable : subNorm.tables)
@ -920,19 +990,19 @@ void Unifier::tryUnifyNormalizedTypes(
return reportError(*e);
}
if (!found)
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
}
if (!subNorm.functions.isNever())
{
if (superNorm.functions.isNever())
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
for (TypeId superFun : *superNorm.functions.parts)
{
Unifier innerState = makeChildUnifier();
const FunctionTypeVar* superFtv = get<FunctionTypeVar>(superFun);
if (!superFtv)
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
TypePackId tgt = innerState.tryApplyOverloadedFunction(subTy, subNorm.functions, superFtv->argTypes);
innerState.tryUnify_(tgt, superFtv->retTypes);
if (innerState.errors.empty())
@ -940,7 +1010,7 @@ void Unifier::tryUnifyNormalizedTypes(
else if (auto e = hasUnificationTooComplex(innerState.errors))
return reportError(*e);
else
return reportError(location, TypeMismatch{superTy, subTy, reason, error});
return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()});
}
}
@ -1306,7 +1376,7 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
// If both are at the end, we're done
if (!superIter.good() && !subIter.good())
{
if (subTpv->tail && superTpv->tail)
if (!FFlag::LuauTxnLogTypePackIterator && subTpv->tail && superTpv->tail)
{
tryUnify_(*subTpv->tail, *superTpv->tail);
break;
@ -1314,10 +1384,27 @@ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCal
const bool lFreeTail = superTpv->tail && log.getMutable<FreeTypePack>(log.follow(*superTpv->tail)) != nullptr;
const bool rFreeTail = subTpv->tail && log.getMutable<FreeTypePack>(log.follow(*subTpv->tail)) != nullptr;
if (lFreeTail)
if (FFlag::LuauTxnLogTypePackIterator && lFreeTail && rFreeTail)
{
tryUnify_(*subTpv->tail, *superTpv->tail);
}
else if (lFreeTail)
{
tryUnify_(emptyTp, *superTpv->tail);
}
else if (rFreeTail)
{
tryUnify_(emptyTp, *subTpv->tail);
}
else if (FFlag::LuauTxnLogTypePackIterator && subTpv->tail && superTpv->tail)
{
if (log.getMutable<VariadicTypePack>(superIter.packId))
tryUnifyVariadics(subIter.packId, superIter.packId, false, int(subIter.index));
else if (log.getMutable<VariadicTypePack>(subIter.packId))
tryUnifyVariadics(superIter.packId, subIter.packId, true, int(superIter.index));
else
tryUnify_(*subTpv->tail, *superTpv->tail);
}
break;
}
@ -1407,7 +1494,7 @@ void Unifier::tryUnifyPrimitives(TypeId subTy, TypeId superTy)
ice("passed non primitive types to unifyPrimitives");
if (superPrim->type != subPrim->type)
reportError(location, TypeMismatch{superTy, subTy});
reportError(location, TypeMismatch{superTy, subTy, mismatchContext()});
}
void Unifier::tryUnifySingletons(TypeId subTy, TypeId superTy)
@ -1428,7 +1515,7 @@ void Unifier::tryUnifySingletons(TypeId subTy, TypeId superTy)
if (superPrim && superPrim->type == PrimitiveTypeVar::String && get<StringSingleton>(subSingleton) && variance == Covariant)
return;
reportError(location, TypeMismatch{superTy, subTy});
reportError(location, TypeMismatch{superTy, subTy, mismatchContext()});
}
void Unifier::tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCall)
@ -1471,14 +1558,14 @@ void Unifier::tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCal
{
numGenerics = std::min(superFunction->generics.size(), subFunction->generics.size());
reportError(location, TypeMismatch{superTy, subTy, "different number of generic type parameters"});
reportError(location, TypeMismatch{superTy, subTy, "different number of generic type parameters", mismatchContext()});
}
if (numGenericPacks != subFunction->genericPacks.size())
{
numGenericPacks = std::min(superFunction->genericPacks.size(), subFunction->genericPacks.size());
reportError(location, TypeMismatch{superTy, subTy, "different number of generic type pack parameters"});
reportError(location, TypeMismatch{superTy, subTy, "different number of generic type pack parameters", mismatchContext()});
}
for (size_t i = 0; i < numGenerics; i++)
@ -1506,9 +1593,9 @@ void Unifier::tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCal
reportError(*e);
else if (!innerState.errors.empty() && innerState.firstPackErrorPos)
reportError(location, TypeMismatch{superTy, subTy, format("Argument #%d type is not compatible.", *innerState.firstPackErrorPos),
innerState.errors.front()});
innerState.errors.front(), mismatchContext()});
else if (!innerState.errors.empty())
reportError(location, TypeMismatch{superTy, subTy, "", innerState.errors.front()});
reportError(location, TypeMismatch{superTy, subTy, "", innerState.errors.front(), mismatchContext()});
innerState.ctx = CountMismatch::FunctionResult;
innerState.tryUnify_(subFunction->retTypes, superFunction->retTypes);
@ -1518,12 +1605,12 @@ void Unifier::tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCal
if (auto e = hasUnificationTooComplex(innerState.errors))
reportError(*e);
else if (!innerState.errors.empty() && size(superFunction->retTypes) == 1 && finite(superFunction->retTypes))
reportError(location, TypeMismatch{superTy, subTy, "Return type is not compatible.", innerState.errors.front()});
reportError(location, TypeMismatch{superTy, subTy, "Return type is not compatible.", innerState.errors.front(), mismatchContext()});
else if (!innerState.errors.empty() && innerState.firstPackErrorPos)
reportError(location, TypeMismatch{superTy, subTy, format("Return #%d type is not compatible.", *innerState.firstPackErrorPos),
innerState.errors.front()});
innerState.errors.front(), mismatchContext()});
else if (!innerState.errors.empty())
reportError(location, TypeMismatch{superTy, subTy, "", innerState.errors.front()});
reportError(location, TypeMismatch{superTy, subTy, "", innerState.errors.front(), mismatchContext()});
}
log.concat(std::move(innerState.log));
@ -1700,10 +1787,10 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
// Recursive unification can change the txn log, and invalidate the old
// table. If we detect that this has happened, we start over, with the updated
// txn log.
TypeId superTyNew = FFlag::LuauScalarShapeUnifyToMtOwner ? log.follow(superTy) : superTy;
TypeId subTyNew = FFlag::LuauScalarShapeUnifyToMtOwner ? log.follow(subTy) : subTy;
TypeId superTyNew = FFlag::LuauScalarShapeUnifyToMtOwner2 ? log.follow(superTy) : superTy;
TypeId subTyNew = FFlag::LuauScalarShapeUnifyToMtOwner2 ? log.follow(subTy) : subTy;
if (FFlag::LuauScalarShapeUnifyToMtOwner)
if (FFlag::LuauScalarShapeUnifyToMtOwner2)
{
// If one of the types stopped being a table altogether, we need to restart from the top
if ((superTy != superTyNew || subTy != subTyNew) && errors.empty())
@ -1771,11 +1858,21 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
else
extraProperties.push_back(name);
TypeId superTyNew = FFlag::LuauScalarShapeUnifyToMtOwner2 ? log.follow(superTy) : superTy;
TypeId subTyNew = FFlag::LuauScalarShapeUnifyToMtOwner2 ? log.follow(subTy) : subTy;
if (FFlag::LuauScalarShapeUnifyToMtOwner2)
{
// If one of the types stopped being a table altogether, we need to restart from the top
if ((superTy != superTyNew || subTy != subTyNew) && errors.empty())
return tryUnify(subTy, superTy, false, isIntersection);
}
// Recursive unification can change the txn log, and invalidate the old
// table. If we detect that this has happened, we start over, with the updated
// txn log.
TableTypeVar* newSuperTable = log.getMutable<TableTypeVar>(superTy);
TableTypeVar* newSubTable = log.getMutable<TableTypeVar>(subTy);
TableTypeVar* newSuperTable = log.getMutable<TableTypeVar>(superTyNew);
TableTypeVar* newSubTable = log.getMutable<TableTypeVar>(subTyNew);
if (superTable != newSuperTable || (subTable != newSubTable && subTable != instantiatedSubTable))
{
if (errors.empty())
@ -1829,8 +1926,19 @@ void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection)
}
// Changing the indexer can invalidate the table pointers.
superTable = log.getMutable<TableTypeVar>(superTy);
subTable = log.getMutable<TableTypeVar>(subTy);
if (FFlag::LuauScalarShapeUnifyToMtOwner2)
{
superTable = log.getMutable<TableTypeVar>(log.follow(superTy));
subTable = log.getMutable<TableTypeVar>(log.follow(subTy));
if (!superTable || !subTable)
return;
}
else
{
superTable = log.getMutable<TableTypeVar>(superTy);
subTable = log.getMutable<TableTypeVar>(subTy);
}
if (!missingProperties.empty())
{
@ -1872,20 +1980,23 @@ void Unifier::tryUnifyScalarShape(TypeId subTy, TypeId superTy, bool reversed)
TypeId osubTy = subTy;
TypeId osuperTy = superTy;
if (FFlag::LuauUninhabitedSubAnything && !normalizer->isInhabited(subTy))
return;
if (reversed)
std::swap(subTy, superTy);
TableTypeVar* superTable = log.getMutable<TableTypeVar>(superTy);
if (!superTable || superTable->state != TableState::Free)
return reportError(location, TypeMismatch{osuperTy, osubTy});
return reportError(location, TypeMismatch{osuperTy, osubTy, mismatchContext()});
auto fail = [&](std::optional<TypeError> e) {
std::string reason = "The former's metatable does not satisfy the requirements.";
if (e)
reportError(location, TypeMismatch{osuperTy, osubTy, reason, *e});
reportError(location, TypeMismatch{osuperTy, osubTy, reason, *e, mismatchContext()});
else
reportError(location, TypeMismatch{osuperTy, osubTy, reason});
reportError(location, TypeMismatch{osuperTy, osubTy, reason, mismatchContext()});
};
// Given t1 where t1 = { lower: (t1) -> (a, b...) }
@ -1902,7 +2013,7 @@ void Unifier::tryUnifyScalarShape(TypeId subTy, TypeId superTy, bool reversed)
Unifier child = makeChildUnifier();
child.tryUnify_(ty, superTy);
if (FFlag::LuauScalarShapeUnifyToMtOwner)
if (FFlag::LuauScalarShapeUnifyToMtOwner2)
{
// To perform subtype <: free table unification, we have tried to unify (subtype's metatable) <: free table
// There is a chance that it was unified with the origial subtype, but then, (subtype's metatable) <: subtype could've failed
@ -1923,7 +2034,7 @@ void Unifier::tryUnifyScalarShape(TypeId subTy, TypeId superTy, bool reversed)
log.concat(std::move(child.log));
if (FFlag::LuauScalarShapeUnifyToMtOwner)
if (FFlag::LuauScalarShapeUnifyToMtOwner2)
{
// To perform subtype <: free table unification, we have tried to unify (subtype's metatable) <: free table
// We return success because subtype <: free table which means that correct unification is to replace free table with the subtype
@ -1939,7 +2050,7 @@ void Unifier::tryUnifyScalarShape(TypeId subTy, TypeId superTy, bool reversed)
}
}
reportError(location, TypeMismatch{osuperTy, osubTy});
reportError(location, TypeMismatch{osuperTy, osubTy, mismatchContext()});
return;
}
@ -1969,7 +2080,7 @@ void Unifier::tryUnifyWithMetatable(TypeId subTy, TypeId superTy, bool reversed)
if (!superMetatable)
ice("tryUnifyMetatable invoked with non-metatable TypeVar");
TypeError mismatchError = TypeError{location, TypeMismatch{reversed ? subTy : superTy, reversed ? superTy : subTy}};
TypeError mismatchError = TypeError{location, TypeMismatch{reversed ? subTy : superTy, reversed ? superTy : subTy, mismatchContext()}};
if (const MetatableTypeVar* subMetatable = log.getMutable<MetatableTypeVar>(subTy))
{
@ -1980,7 +2091,8 @@ void Unifier::tryUnifyWithMetatable(TypeId subTy, TypeId superTy, bool reversed)
if (auto e = hasUnificationTooComplex(innerState.errors))
reportError(*e);
else if (!innerState.errors.empty())
reportError(location, TypeMismatch{reversed ? subTy : superTy, reversed ? superTy : subTy, "", innerState.errors.front()});
reportError(
location, TypeMismatch{reversed ? subTy : superTy, reversed ? superTy : subTy, "", innerState.errors.front(), mismatchContext()});
log.concat(std::move(innerState.log));
}
@ -2017,8 +2129,8 @@ void Unifier::tryUnifyWithMetatable(TypeId subTy, TypeId superTy, bool reversed)
if (auto e = hasUnificationTooComplex(innerState.errors))
reportError(*e);
else if (!innerState.errors.empty())
reportError(
TypeError{location, TypeMismatch{reversed ? subTy : superTy, reversed ? superTy : subTy, "", innerState.errors.front()}});
reportError(TypeError{location,
TypeMismatch{reversed ? subTy : superTy, reversed ? superTy : subTy, "", innerState.errors.front(), mismatchContext()}});
else if (!missingProperty)
{
log.concat(std::move(innerState.log));
@ -2057,9 +2169,9 @@ void Unifier::tryUnifyWithClass(TypeId subTy, TypeId superTy, bool reversed)
auto fail = [&]() {
if (!reversed)
reportError(location, TypeMismatch{superTy, subTy});
reportError(location, TypeMismatch{superTy, subTy, mismatchContext()});
else
reportError(location, TypeMismatch{subTy, superTy});
reportError(location, TypeMismatch{subTy, superTy, mismatchContext()});
};
const ClassTypeVar* superClass = get<ClassTypeVar>(superTy);
@ -2155,7 +2267,7 @@ void Unifier::tryUnifyTypeWithNegation(TypeId subTy, TypeId superTy)
Unifier state = makeChildUnifier();
state.tryUnifyNormalizedTypes(subTy, superTy, *subNorm, *superNorm, "");
if (state.errors.empty())
reportError(location, TypeMismatch{superTy, subTy});
reportError(location, TypeMismatch{superTy, subTy, mismatchContext()});
}
void Unifier::tryUnifyNegationWithType(TypeId subTy, TypeId superTy)
@ -2165,7 +2277,7 @@ void Unifier::tryUnifyNegationWithType(TypeId subTy, TypeId superTy)
ice("tryUnifyNegationWithType subTy must be a negation type");
// TODO: ~T </: U iff T <: U
reportError(location, TypeMismatch{superTy, subTy});
reportError(location, TypeMismatch{superTy, subTy, mismatchContext()});
}
static void queueTypePack(std::vector<TypeId>& queue, DenseHashSet<TypePackId>& seenTypePacks, Unifier& state, TypePackId a, TypePackId anyTypePack)
@ -2200,9 +2312,11 @@ void Unifier::tryUnifyVariadics(TypePackId subTp, TypePackId superTp, bool rever
if (!superVariadic)
ice("passed non-variadic pack to tryUnifyVariadics");
if (const VariadicTypePack* subVariadic = get<VariadicTypePack>(subTp))
if (const VariadicTypePack* subVariadic = FFlag::LuauTxnLogTypePackIterator ? log.get<VariadicTypePack>(subTp) : get<VariadicTypePack>(subTp))
{
tryUnify_(reversed ? superVariadic->ty : subVariadic->ty, reversed ? subVariadic->ty : superVariadic->ty);
else if (get<TypePack>(subTp))
}
else if (FFlag::LuauTxnLogTypePackIterator ? log.get<TypePack>(subTp) : get<TypePack>(subTp))
{
TypePackIterator subIter = begin(subTp, &log);
TypePackIterator subEnd = end(subTp);
@ -2350,6 +2464,24 @@ std::optional<TypeId> Unifier::findTablePropertyRespectingMeta(TypeId lhsType, N
return Luau::findTablePropertyRespectingMeta(singletonTypes, errors, lhsType, name, location);
}
TxnLog Unifier::combineLogsIntoIntersection(std::vector<TxnLog> logs)
{
LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution);
TxnLog result;
for (TxnLog& log : logs)
result.concatAsIntersections(std::move(log), NotNull{types});
return result;
}
TxnLog Unifier::combineLogsIntoUnion(std::vector<TxnLog> logs)
{
LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution);
TxnLog result;
for (TxnLog& log : logs)
result.concatAsUnion(std::move(log), NotNull{types});
return result;
}
bool Unifier::occursCheck(TypeId needle, TypeId haystack)
{
sharedState.tempSeenTy.clear();
@ -2491,7 +2623,7 @@ void Unifier::checkChildUnifierTypeMismatch(const ErrorVec& innerErrors, TypeId
if (auto e = hasUnificationTooComplex(innerErrors))
reportError(*e);
else if (!innerErrors.empty())
reportError(location, TypeMismatch{wantedType, givenType});
reportError(location, TypeMismatch{wantedType, givenType, mismatchContext()});
}
void Unifier::checkChildUnifierTypeMismatch(const ErrorVec& innerErrors, const std::string& prop, TypeId wantedType, TypeId givenType)
@ -2499,8 +2631,8 @@ void Unifier::checkChildUnifierTypeMismatch(const ErrorVec& innerErrors, const s
if (auto e = hasUnificationTooComplex(innerErrors))
reportError(*e);
else if (!innerErrors.empty())
reportError(
TypeError{location, TypeMismatch{wantedType, givenType, format("Property '%s' is not compatible.", prop.c_str()), innerErrors.front()}});
reportError(TypeError{location,
TypeMismatch{wantedType, givenType, format("Property '%s' is not compatible.", prop.c_str()), innerErrors.front(), mismatchContext()}});
}
void Unifier::ice(const std::string& message, const Location& location)

View File

@ -50,6 +50,20 @@ struct Position
{
return *this == rhs || *this > rhs;
}
void shift(const Position& start, const Position& oldEnd, const Position& newEnd)
{
if (*this >= start)
{
if (this->line > oldEnd.line)
this->line += (newEnd.line - oldEnd.line);
else
{
this->line = newEnd.line;
this->column += (newEnd.column - oldEnd.column);
}
}
}
};
struct Location
@ -93,6 +107,10 @@ struct Location
{
return begin <= l.begin && end >= l.end;
}
bool overlaps(const Location& l) const
{
return (begin <= l.begin && end >= l.begin) || (begin <= l.end && end >= l.end) || (begin >= l.begin && end <= l.end);
}
bool contains(const Position& p) const
{
return begin <= p && p < end;
@ -101,6 +119,18 @@ struct Location
{
return begin <= p && p <= end;
}
void extend(const Location& other)
{
if (other.begin < begin)
begin = other.begin;
if (other.end > end)
end = other.end;
}
void shift(const Position& start, const Position& oldEnd, const Position& newEnd)
{
begin.shift(start, oldEnd, newEnd);
end.shift(start, oldEnd, newEnd);
}
};
std::string toString(const Position& position);

View File

@ -24,9 +24,6 @@ LUAU_DYNAMIC_FASTFLAGVARIABLE(LuaReportParseIntegerIssues, false)
LUAU_FASTFLAGVARIABLE(LuauInterpolatedStringBaseSupport, false)
LUAU_FASTFLAGVARIABLE(LuauCommaParenWarnings, false)
LUAU_FASTFLAGVARIABLE(LuauTableConstructorRecovery, false)
LUAU_FASTFLAGVARIABLE(LuauParserErrorsOnMissingDefaultTypePackArgument, false)
bool lua_telemetry_parsed_out_of_range_bin_integer = false;
@ -1084,7 +1081,7 @@ void Parser::parseExprList(TempVector<AstExpr*>& result)
{
nextLexeme();
if (FFlag::LuauCommaParenWarnings && lexer.current().type == ')')
if (lexer.current().type == ')')
{
report(lexer.current().location, "Expected expression after ',' but got ')' instead");
break;
@ -1179,7 +1176,7 @@ AstTypePack* Parser::parseTypeList(TempVector<AstType*>& result, TempVector<std:
nextLexeme();
if (FFlag::LuauCommaParenWarnings && lexer.current().type == ')')
if (lexer.current().type == ')')
{
report(lexer.current().location, "Expected type after ',' but got ')' instead");
break;
@ -2317,8 +2314,7 @@ AstExpr* Parser::parseTableConstructor()
while (lexer.current().type != '}')
{
if (FFlag::LuauTableConstructorRecovery)
lastElementIndent = lexer.current().location.begin.column;
lastElementIndent = lexer.current().location.begin.column;
if (lexer.current().type == '[')
{
@ -2364,8 +2360,7 @@ AstExpr* Parser::parseTableConstructor()
{
nextLexeme();
}
else if (FFlag::LuauTableConstructorRecovery && (lexer.current().type == '[' || lexer.current().type == Lexeme::Name) &&
lexer.current().location.begin.column == lastElementIndent)
else if ((lexer.current().type == '[' || lexer.current().type == Lexeme::Name) && lexer.current().location.begin.column == lastElementIndent)
{
report(lexer.current().location, "Expected ',' after table constructor element");
}
@ -2556,7 +2551,7 @@ std::pair<AstArray<AstGenericType>, AstArray<AstGenericTypePack>> Parser::parseG
{
nextLexeme();
if (FFlag::LuauCommaParenWarnings && lexer.current().type == '>')
if (lexer.current().type == '>')
{
report(lexer.current().location, "Expected type after ',' but got '>' instead");
break;

View File

@ -1895,10 +1895,7 @@ void BytecodeBuilder::dumpInstruction(const uint32_t* code, std::string& result,
case LOP_CAPTURE:
formatAppend(result, "CAPTURE %s %c%d\n",
LUAU_INSN_A(insn) == LCT_UPVAL ? "UPVAL"
: LUAU_INSN_A(insn) == LCT_REF ? "REF"
: LUAU_INSN_A(insn) == LCT_VAL ? "VAL"
: "",
LUAU_INSN_A(insn) == LCT_UPVAL ? "UPVAL" : LUAU_INSN_A(insn) == LCT_REF ? "REF" : LUAU_INSN_A(insn) == LCT_VAL ? "VAL" : "",
LUAU_INSN_A(insn) == LCT_UPVAL ? 'U' : 'R', LUAU_INSN_B(insn));
break;

View File

@ -26,6 +26,7 @@ LUAU_FASTINTVARIABLE(LuauCompileInlineThresholdMaxBoost, 300)
LUAU_FASTINTVARIABLE(LuauCompileInlineDepth, 5)
LUAU_FASTFLAG(LuauInterpolatedStringBaseSupport)
LUAU_FASTFLAGVARIABLE(LuauMultiAssignmentConflictFix, false)
namespace Luau
{
@ -2977,16 +2978,46 @@ struct Compiler
Visitor visitor(this);
// mark any registers that are used *after* assignment as conflicting
for (size_t i = 0; i < vars.size(); ++i)
if (FFlag::LuauMultiAssignmentConflictFix)
{
const LValue& li = vars[i].lvalue;
// mark any registers that are used *after* assignment as conflicting
if (i < values.size)
values.data[i]->visit(&visitor);
// first we go through assignments to locals, since they are performed before assignments to other l-values
for (size_t i = 0; i < vars.size(); ++i)
{
const LValue& li = vars[i].lvalue;
if (li.kind == LValue::Kind_Local)
visitor.assigned[li.reg] = true;
if (li.kind == LValue::Kind_Local)
{
if (i < values.size)
values.data[i]->visit(&visitor);
visitor.assigned[li.reg] = true;
}
}
// and now we handle all other l-values
for (size_t i = 0; i < vars.size(); ++i)
{
const LValue& li = vars[i].lvalue;
if (li.kind != LValue::Kind_Local && i < values.size)
values.data[i]->visit(&visitor);
}
}
else
{
// mark any registers that are used *after* assignment as conflicting
for (size_t i = 0; i < vars.size(); ++i)
{
const LValue& li = vars[i].lvalue;
if (i < values.size)
values.data[i]->visit(&visitor);
if (li.kind == LValue::Kind_Local)
visitor.assigned[li.reg] = true;
}
}
// mark any registers used in trailing expressions as conflicting as well

View File

@ -112,7 +112,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/include/Luau/Constraint.h
Analysis/include/Luau/ConstraintGraphBuilder.h
Analysis/include/Luau/ConstraintSolver.h
Analysis/include/Luau/DataFlowGraphBuilder.h
Analysis/include/Luau/DataFlowGraph.h
Analysis/include/Luau/DcrLogger.h
Analysis/include/Luau/Def.h
Analysis/include/Luau/Documentation.h
@ -166,7 +166,7 @@ target_sources(Luau.Analysis PRIVATE
Analysis/src/Constraint.cpp
Analysis/src/ConstraintGraphBuilder.cpp
Analysis/src/ConstraintSolver.cpp
Analysis/src/DataFlowGraphBuilder.cpp
Analysis/src/DataFlowGraph.cpp
Analysis/src/DcrLogger.cpp
Analysis/src/Def.cpp
Analysis/src/EmbeddedBuiltinDefinitions.cpp
@ -298,15 +298,16 @@ endif()
if(TARGET Luau.UnitTest)
# Luau.UnitTest Sources
target_sources(Luau.UnitTest PRIVATE
tests/AstQueryDsl.cpp
tests/AstQueryDsl.h
tests/ClassFixture.cpp
tests/ClassFixture.h
tests/ConstraintGraphBuilderFixture.cpp
tests/ConstraintGraphBuilderFixture.h
tests/Fixture.cpp
tests/Fixture.h
tests/IostreamOptional.h
tests/ScopedFlags.h
tests/AstQueryDsl.cpp
tests/ConstraintGraphBuilderFixture.cpp
tests/Fixture.cpp
tests/AssemblyBuilderA64.test.cpp
tests/AssemblyBuilderX64.test.cpp
tests/AstJsonEncoder.test.cpp
tests/AstQuery.test.cpp
@ -318,7 +319,7 @@ if(TARGET Luau.UnitTest)
tests/Config.test.cpp
tests/ConstraintSolver.test.cpp
tests/CostModel.test.cpp
tests/DataFlowGraphBuilder.test.cpp
tests/DataFlowGraph.test.cpp
tests/Error.test.cpp
tests/Frontend.test.cpp
tests/JsonEmitter.test.cpp

View File

@ -22,6 +22,21 @@ static time_t timegm(struct tm* timep)
{
return _mkgmtime(timep);
}
#elif defined(__FreeBSD__)
static tm* gmtime_r(const time_t* timep, tm* result)
{
return gmtime_s(timep, result) == 0 ? result : NULL;
}
static tm* localtime_r(const time_t* timep, tm* result)
{
return localtime_s(timep, result) == 0 ? result : NULL;
}
static time_t timegm(struct tm* timep)
{
return mktime(timep);
}
#endif
static int os_clock(lua_State* L)

View File

@ -91,8 +91,6 @@ TEST_CASE_FIXTURE(DocumentationSymbolFixture, "class_method")
TEST_CASE_FIXTURE(DocumentationSymbolFixture, "overloaded_class_method")
{
ScopedFastFlag luauCheckOverloadedDocSymbol{"LuauCheckOverloadedDocSymbol", true};
loadDefinition(R"(
declare class Foo
function bar(self, x: string): number
@ -127,8 +125,6 @@ TEST_CASE_FIXTURE(DocumentationSymbolFixture, "table_function_prop")
TEST_CASE_FIXTURE(DocumentationSymbolFixture, "table_overloaded_function_prop")
{
ScopedFastFlag luauCheckOverloadedDocSymbol{"LuauCheckOverloadedDocSymbol", true};
loadDefinition(R"(
declare Foo: {
new: ((number) -> string) & ((string) -> number)

113
tests/ClassFixture.cpp Normal file
View File

@ -0,0 +1,113 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "ClassFixture.h"
#include "Luau/BuiltinDefinitions.h"
using std::nullopt;
namespace Luau
{
ClassFixture::ClassFixture()
{
TypeArena& arena = typeChecker.globalTypes;
TypeId numberType = typeChecker.numberType;
unfreeze(arena);
TypeId baseClassInstanceType = arena.addType(ClassTypeVar{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(baseClassInstanceType)->props = {
{"BaseMethod", {makeFunction(arena, baseClassInstanceType, {numberType}, {})}},
{"BaseField", {numberType}},
};
TypeId baseClassType = arena.addType(ClassTypeVar{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(baseClassType)->props = {
{"StaticMethod", {makeFunction(arena, nullopt, {}, {numberType})}},
{"Clone", {makeFunction(arena, nullopt, {baseClassInstanceType}, {baseClassInstanceType})}},
{"New", {makeFunction(arena, nullopt, {}, {baseClassInstanceType})}},
};
typeChecker.globalScope->exportedTypeBindings["BaseClass"] = TypeFun{{}, baseClassInstanceType};
addGlobalBinding(frontend, "BaseClass", baseClassType, "@test");
TypeId childClassInstanceType = arena.addType(ClassTypeVar{"ChildClass", {}, baseClassInstanceType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(childClassInstanceType)->props = {
{"Method", {makeFunction(arena, childClassInstanceType, {}, {typeChecker.stringType})}},
};
TypeId childClassType = arena.addType(ClassTypeVar{"ChildClass", {}, baseClassType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(childClassType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {childClassInstanceType})}},
};
typeChecker.globalScope->exportedTypeBindings["ChildClass"] = TypeFun{{}, childClassInstanceType};
addGlobalBinding(frontend, "ChildClass", childClassType, "@test");
TypeId grandChildInstanceType = arena.addType(ClassTypeVar{"GrandChild", {}, childClassInstanceType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(grandChildInstanceType)->props = {
{"Method", {makeFunction(arena, grandChildInstanceType, {}, {typeChecker.stringType})}},
};
TypeId grandChildType = arena.addType(ClassTypeVar{"GrandChild", {}, baseClassType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(grandChildType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {grandChildInstanceType})}},
};
typeChecker.globalScope->exportedTypeBindings["GrandChild"] = TypeFun{{}, grandChildInstanceType};
addGlobalBinding(frontend, "GrandChild", childClassType, "@test");
TypeId anotherChildInstanceType = arena.addType(ClassTypeVar{"AnotherChild", {}, baseClassInstanceType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(anotherChildInstanceType)->props = {
{"Method", {makeFunction(arena, anotherChildInstanceType, {}, {typeChecker.stringType})}},
};
TypeId anotherChildType = arena.addType(ClassTypeVar{"AnotherChild", {}, baseClassType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(anotherChildType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {anotherChildInstanceType})}},
};
typeChecker.globalScope->exportedTypeBindings["AnotherChild"] = TypeFun{{}, anotherChildInstanceType};
addGlobalBinding(frontend, "AnotherChild", childClassType, "@test");
TypeId unrelatedClassInstanceType = arena.addType(ClassTypeVar{"UnrelatedClass", {}, nullopt, nullopt, {}, {}, "Test"});
TypeId unrelatedClassType = arena.addType(ClassTypeVar{"UnrelatedClass", {}, nullopt, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(unrelatedClassType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {unrelatedClassInstanceType})}},
};
typeChecker.globalScope->exportedTypeBindings["UnrelatedClass"] = TypeFun{{}, unrelatedClassInstanceType};
addGlobalBinding(frontend, "UnrelatedClass", unrelatedClassType, "@test");
TypeId vector2MetaType = arena.addType(TableTypeVar{});
TypeId vector2InstanceType = arena.addType(ClassTypeVar{"Vector2", {}, nullopt, vector2MetaType, {}, {}, "Test"});
getMutable<ClassTypeVar>(vector2InstanceType)->props = {
{"X", {numberType}},
{"Y", {numberType}},
};
TypeId vector2Type = arena.addType(ClassTypeVar{"Vector2", {}, nullopt, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(vector2Type)->props = {
{"New", {makeFunction(arena, nullopt, {numberType, numberType}, {vector2InstanceType})}},
};
getMutable<TableTypeVar>(vector2MetaType)->props = {
{"__add", {makeFunction(arena, nullopt, {vector2InstanceType, vector2InstanceType}, {vector2InstanceType})}},
};
typeChecker.globalScope->exportedTypeBindings["Vector2"] = TypeFun{{}, vector2InstanceType};
addGlobalBinding(frontend, "Vector2", vector2Type, "@test");
TypeId callableClassMetaType = arena.addType(TableTypeVar{});
TypeId callableClassType = arena.addType(ClassTypeVar{"CallableClass", {}, nullopt, callableClassMetaType, {}, {}, "Test"});
getMutable<TableTypeVar>(callableClassMetaType)->props = {
{"__call", {makeFunction(arena, nullopt, {callableClassType, typeChecker.stringType}, {typeChecker.numberType})}},
};
typeChecker.globalScope->exportedTypeBindings["CallableClass"] = TypeFun{{}, callableClassType};
for (const auto& [name, tf] : typeChecker.globalScope->exportedTypeBindings)
persist(tf.type);
freeze(arena);
}
} // namespace Luau

13
tests/ClassFixture.h Normal file
View File

@ -0,0 +1,13 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Fixture.h"
namespace Luau
{
struct ClassFixture : BuiltinsFixture
{
ClassFixture();
};
} // namespace Luau

View File

@ -6751,6 +6751,21 @@ ADD R4 R0 R1
MOVE R0 R2
MOVE R1 R3
RETURN R0 0
)");
ScopedFastFlag luauMultiAssignmentConflictFix{"LuauMultiAssignmentConflictFix", true};
// because we perform assignments to complex l-values after assignments to locals, we make sure register conflicts are tracked accordingly
CHECK_EQ("\n" + compileFunction0(R"(
local a, b = ...
a[1], b = b, b + 1
)"),
R"(
GETVARARGS R0 2
ADDK R2 R1 K0
SETTABLEN R1 R0 1
MOVE R1 R2
RETURN R0 0
)");
}

View File

@ -1,5 +1,5 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/DataFlowGraphBuilder.h"
#include "Luau/DataFlowGraph.h"
#include "Luau/Error.h"
#include "Luau/Parser.h"

View File

@ -11,7 +11,6 @@
using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(LuauIceExceptionInheritanceChange);
TEST_SUITE_BEGIN("ModuleTests");
@ -279,14 +278,7 @@ TEST_CASE_FIXTURE(Fixture, "clone_recursion_limit")
TypeArena dest;
CloneState cloneState;
if (FFlag::LuauIceExceptionInheritanceChange)
{
CHECK_THROWS_AS(clone(table, dest, cloneState), RecursionLimitException);
}
else
{
CHECK_THROWS_AS(clone(table, dest, cloneState), RecursionLimitException_DEPRECATED);
}
CHECK_THROWS_AS(clone(table, dest, cloneState), RecursionLimitException);
}
TEST_CASE_FIXTURE(Fixture, "any_persistance_does_not_leak")

View File

@ -9,6 +9,8 @@
using Luau::NotNull;
static_assert(!std::is_convertible<NotNull<int>, bool>::value, "NotNull<T> ought not to be convertible into bool");
namespace
{

View File

@ -2723,8 +2723,6 @@ TEST_CASE_FIXTURE(Fixture, "error_message_for_using_function_as_type_annotation"
TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_an_extra_comma_at_the_end_of_a_function_argument_list")
{
ScopedFastFlag sff{"LuauCommaParenWarnings", true};
ParseResult result = tryParse(R"(
foo(a, b, c,)
)");
@ -2737,8 +2735,6 @@ TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_an_extra_comma_at_the
TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_an_extra_comma_at_the_end_of_a_function_parameter_list")
{
ScopedFastFlag sff{"LuauCommaParenWarnings", true};
ParseResult result = tryParse(R"(
export type VisitFn = (
any,
@ -2754,8 +2750,6 @@ TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_an_extra_comma_at_the
TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_an_extra_comma_at_the_end_of_a_generic_parameter_list")
{
ScopedFastFlag sff{"LuauCommaParenWarnings", true};
ParseResult result = tryParse(R"(
export type VisitFn = <A, B,>(a: A, b: B) -> ()
)");
@ -2778,8 +2772,6 @@ TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_an_extra_comma_at_the
TEST_CASE_FIXTURE(Fixture, "get_a_nice_error_when_there_is_no_comma_between_table_members")
{
ScopedFastFlag luauTableConstructorRecovery{"LuauTableConstructorRecovery", true};
ParseResult result = tryParse(R"(
local t = {
first = 1

View File

@ -10,7 +10,6 @@
using namespace Luau;
LUAU_FASTFLAG(LuauRecursiveTypeParameterRestriction);
LUAU_FASTFLAG(LuauFunctionReturnStringificationFixup);
TEST_SUITE_BEGIN("ToString");
@ -83,7 +82,7 @@ TEST_CASE_FIXTURE(Fixture, "table_respects_use_line_break")
ToStringOptions opts;
opts.useLineBreaks = true;
opts.indent = true;
opts.DEPRECATED_indent = true;
//clang-format off
CHECK_EQ("{|\n"
@ -568,10 +567,7 @@ TEST_CASE_FIXTURE(Fixture, "no_parentheses_around_return_type_if_pack_has_an_emp
TypeId functionType = arena.addType(FunctionTypeVar{argList, emptyTail});
if (FFlag::LuauFunctionReturnStringificationFixup)
CHECK("(string) -> string" == toString(functionType));
else
CHECK("(string) -> (string)" == toString(functionType));
CHECK("(string) -> string" == toString(functionType));
}
TEST_CASE_FIXTURE(Fixture, "no_parentheses_around_cyclic_function_type_in_union")

View File

@ -9,6 +9,7 @@ using namespace Luau;
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(LuauNoMoreGlobalSingletonTypes)
LUAU_FASTFLAG(LuauTypeMismatchInvarianceInError)
TEST_SUITE_BEGIN("TypeAliases");
@ -199,9 +200,15 @@ TEST_CASE_FIXTURE(Fixture, "generic_aliases")
LUAU_REQUIRE_ERROR_COUNT(1, result);
const char* expectedError = "Type '{ v: string }' could not be converted into 'T<number>'\n"
"caused by:\n"
" Property 'v' is not compatible. Type 'string' could not be converted into 'number'";
const char* expectedError;
if (FFlag::LuauTypeMismatchInvarianceInError)
expectedError = "Type '{ v: string }' could not be converted into 'T<number>'\n"
"caused by:\n"
" Property 'v' is not compatible. Type 'string' could not be converted into 'number' in an invariant context";
else
expectedError = "Type '{ v: string }' could not be converted into 'T<number>'\n"
"caused by:\n"
" Property 'v' is not compatible. Type 'string' could not be converted into 'number'";
CHECK(result.errors[0].location == Location{{4, 31}, {4, 44}});
CHECK(toString(result.errors[0]) == expectedError);
@ -220,11 +227,19 @@ TEST_CASE_FIXTURE(Fixture, "dependent_generic_aliases")
LUAU_REQUIRE_ERROR_COUNT(1, result);
const char* expectedError = "Type '{ t: { v: string } }' could not be converted into 'U<number>'\n"
"caused by:\n"
" Property 't' is not compatible. Type '{ v: string }' could not be converted into 'T<number>'\n"
"caused by:\n"
" Property 'v' is not compatible. Type 'string' could not be converted into 'number'";
const char* expectedError;
if (FFlag::LuauTypeMismatchInvarianceInError)
expectedError = "Type '{ t: { v: string } }' could not be converted into 'U<number>'\n"
"caused by:\n"
" Property 't' is not compatible. Type '{ v: string }' could not be converted into 'T<number>'\n"
"caused by:\n"
" Property 'v' is not compatible. Type 'string' could not be converted into 'number' in an invariant context";
else
expectedError = "Type '{ t: { v: string } }' could not be converted into 'U<number>'\n"
"caused by:\n"
" Property 't' is not compatible. Type '{ v: string }' could not be converted into 'T<number>'\n"
"caused by:\n"
" Property 'v' is not compatible. Type 'string' could not be converted into 'number'";
CHECK(result.errors[0].location == Location{{4, 31}, {4, 52}});
CHECK(toString(result.errors[0]) == expectedError);

View File

@ -7,8 +7,6 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauIceExceptionInheritanceChange)
using namespace Luau;
TEST_SUITE_BEGIN("AnnotationTests");

View File

@ -684,20 +684,10 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "select_with_variadic_typepack_tail")
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("string", toString(requireType("foo")));
CHECK_EQ("*error-type*", toString(requireType("bar")));
CHECK_EQ("*error-type*", toString(requireType("baz")));
CHECK_EQ("*error-type*", toString(requireType("quux")));
}
else
{
CHECK_EQ("any", toString(requireType("foo")));
CHECK_EQ("any", toString(requireType("bar")));
CHECK_EQ("any", toString(requireType("baz")));
CHECK_EQ("any", toString(requireType("quux")));
}
CHECK_EQ("any", toString(requireType("foo")));
CHECK_EQ("any", toString(requireType("bar")));
CHECK_EQ("any", toString(requireType("baz")));
CHECK_EQ("any", toString(requireType("quux")));
}
TEST_CASE_FIXTURE(BuiltinsFixture, "select_with_variadic_typepack_tail_and_string_head")
@ -714,19 +704,13 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "select_with_variadic_typepack_tail_and_strin
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("string", toString(requireType("foo")));
CHECK_EQ("string", toString(requireType("bar")));
CHECK_EQ("*error-type*", toString(requireType("baz")));
CHECK_EQ("*error-type*", toString(requireType("quux")));
}
else
{
CHECK_EQ("any", toString(requireType("foo")));
CHECK_EQ("any", toString(requireType("bar")));
CHECK_EQ("any", toString(requireType("baz")));
CHECK_EQ("any", toString(requireType("quux")));
}
CHECK_EQ("any", toString(requireType("bar")));
CHECK_EQ("any", toString(requireType("baz")));
CHECK_EQ("any", toString(requireType("quux")));
}
TEST_CASE_FIXTURE(Fixture, "string_format_as_method")

View File

@ -1,109 +1,18 @@
// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details
#include "Luau/BuiltinDefinitions.h"
#include "Luau/Common.h"
#include "Luau/TypeInfer.h"
#include "Luau/TypeVar.h"
#include "Fixture.h"
#include "ClassFixture.h"
#include "doctest.h"
using namespace Luau;
using std::nullopt;
struct ClassFixture : BuiltinsFixture
{
ClassFixture()
{
TypeArena& arena = typeChecker.globalTypes;
TypeId numberType = typeChecker.numberType;
unfreeze(arena);
TypeId baseClassInstanceType = arena.addType(ClassTypeVar{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(baseClassInstanceType)->props = {
{"BaseMethod", {makeFunction(arena, baseClassInstanceType, {numberType}, {})}},
{"BaseField", {numberType}},
};
TypeId baseClassType = arena.addType(ClassTypeVar{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(baseClassType)->props = {
{"StaticMethod", {makeFunction(arena, nullopt, {}, {numberType})}},
{"Clone", {makeFunction(arena, nullopt, {baseClassInstanceType}, {baseClassInstanceType})}},
{"New", {makeFunction(arena, nullopt, {}, {baseClassInstanceType})}},
};
typeChecker.globalScope->exportedTypeBindings["BaseClass"] = TypeFun{{}, baseClassInstanceType};
addGlobalBinding(frontend, "BaseClass", baseClassType, "@test");
TypeId childClassInstanceType = arena.addType(ClassTypeVar{"ChildClass", {}, baseClassInstanceType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(childClassInstanceType)->props = {
{"Method", {makeFunction(arena, childClassInstanceType, {}, {typeChecker.stringType})}},
};
TypeId childClassType = arena.addType(ClassTypeVar{"ChildClass", {}, baseClassType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(childClassType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {childClassInstanceType})}},
};
typeChecker.globalScope->exportedTypeBindings["ChildClass"] = TypeFun{{}, childClassInstanceType};
addGlobalBinding(frontend, "ChildClass", childClassType, "@test");
TypeId grandChildInstanceType = arena.addType(ClassTypeVar{"GrandChild", {}, childClassInstanceType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(grandChildInstanceType)->props = {
{"Method", {makeFunction(arena, grandChildInstanceType, {}, {typeChecker.stringType})}},
};
TypeId grandChildType = arena.addType(ClassTypeVar{"GrandChild", {}, baseClassType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(grandChildType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {grandChildInstanceType})}},
};
typeChecker.globalScope->exportedTypeBindings["GrandChild"] = TypeFun{{}, grandChildInstanceType};
addGlobalBinding(frontend, "GrandChild", childClassType, "@test");
TypeId anotherChildInstanceType = arena.addType(ClassTypeVar{"AnotherChild", {}, baseClassInstanceType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(anotherChildInstanceType)->props = {
{"Method", {makeFunction(arena, anotherChildInstanceType, {}, {typeChecker.stringType})}},
};
TypeId anotherChildType = arena.addType(ClassTypeVar{"AnotherChild", {}, baseClassType, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(anotherChildType)->props = {
{"New", {makeFunction(arena, nullopt, {}, {anotherChildInstanceType})}},
};
typeChecker.globalScope->exportedTypeBindings["AnotherChild"] = TypeFun{{}, anotherChildInstanceType};
addGlobalBinding(frontend, "AnotherChild", childClassType, "@test");
TypeId vector2MetaType = arena.addType(TableTypeVar{});
TypeId vector2InstanceType = arena.addType(ClassTypeVar{"Vector2", {}, nullopt, vector2MetaType, {}, {}, "Test"});
getMutable<ClassTypeVar>(vector2InstanceType)->props = {
{"X", {numberType}},
{"Y", {numberType}},
};
TypeId vector2Type = arena.addType(ClassTypeVar{"Vector2", {}, nullopt, nullopt, {}, {}, "Test"});
getMutable<ClassTypeVar>(vector2Type)->props = {
{"New", {makeFunction(arena, nullopt, {numberType, numberType}, {vector2InstanceType})}},
};
getMutable<TableTypeVar>(vector2MetaType)->props = {
{"__add", {makeFunction(arena, nullopt, {vector2InstanceType, vector2InstanceType}, {vector2InstanceType})}},
};
typeChecker.globalScope->exportedTypeBindings["Vector2"] = TypeFun{{}, vector2InstanceType};
addGlobalBinding(frontend, "Vector2", vector2Type, "@test");
TypeId callableClassMetaType = arena.addType(TableTypeVar{});
TypeId callableClassType = arena.addType(ClassTypeVar{"CallableClass", {}, nullopt, callableClassMetaType, {}, {}, "Test"});
getMutable<TableTypeVar>(callableClassMetaType)->props = {
{"__call", {makeFunction(arena, nullopt, {callableClassType, typeChecker.stringType}, {typeChecker.numberType})}},
};
typeChecker.globalScope->exportedTypeBindings["CallableClass"] = TypeFun{{}, callableClassType};
for (const auto& [name, tf] : typeChecker.globalScope->exportedTypeBindings)
persist(tf.type);
freeze(arena);
}
};
LUAU_FASTFLAG(LuauTypeMismatchInvarianceInError);
TEST_SUITE_BEGIN("TypeInferClasses");
@ -521,6 +430,56 @@ TEST_CASE_FIXTURE(ClassFixture, "unions_of_intersections_of_classes")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(ClassFixture, "index_instance_property")
{
ScopedFastFlag luauAllowIndexClassParameters{"LuauAllowIndexClassParameters", true};
CheckResult result = check(R"(
local function execute(object: BaseClass, name: string)
print(object[name])
end
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ("Attempting a dynamic property access on type 'BaseClass' is unsafe and may cause exceptions at runtime", toString(result.errors[0]));
}
TEST_CASE_FIXTURE(ClassFixture, "index_instance_property_nonstrict")
{
ScopedFastFlag luauAllowIndexClassParameters{"LuauAllowIndexClassParameters", true};
CheckResult result = check(R"(
--!nonstrict
local function execute(object: BaseClass, name: string)
print(object[name])
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(ClassFixture, "type_mismatch_invariance_required_for_error")
{
CheckResult result = check(R"(
type A = { x: ChildClass }
type B = { x: BaseClass }
local a: A
local b: B = a
)");
LUAU_REQUIRE_ERRORS(result);
if (FFlag::LuauTypeMismatchInvarianceInError)
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
caused by:
Property 'x' is not compatible. Type 'ChildClass' could not be converted into 'BaseClass' in an invariant context)");
else
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
caused by:
Property 'x' is not compatible. Type 'ChildClass' could not be converted into 'BaseClass')");
}
TEST_CASE_FIXTURE(ClassFixture, "callable_classes")
{
ScopedFastFlag luauCallableClasses{"LuauCallableClasses", true};

View File

@ -311,8 +311,6 @@ TEST_CASE_FIXTURE(Fixture, "definitions_documentation_symbols")
TEST_CASE_FIXTURE(Fixture, "definitions_symbols_are_generated_for_recursively_referenced_types")
{
ScopedFastFlag LuauPersistTypesAfterGeneratingDocSyms("LuauPersistTypesAfterGeneratingDocSyms", true);
loadDefinition(R"(
declare class MyClass
function myMethod(self)

View File

@ -230,8 +230,6 @@ TEST_CASE_FIXTURE(Fixture, "too_many_arguments")
TEST_CASE_FIXTURE(Fixture, "too_many_arguments_error_location")
{
ScopedFastFlag sff{"LuauArgMismatchReportFunctionLocation", true};
CheckResult result = check(R"(
--!strict
@ -507,7 +505,9 @@ TEST_CASE_FIXTURE(Fixture, "complicated_return_types_require_an_explicit_annotat
LUAU_REQUIRE_NO_ERRORS(result);
const FunctionTypeVar* functionType = get<FunctionTypeVar>(requireType("most_of_the_natural_numbers"));
TypeId ty = requireType("most_of_the_natural_numbers");
const FunctionTypeVar* functionType = get<FunctionTypeVar>(ty);
REQUIRE_MESSAGE(functionType, "Expected function but got " << toString(ty));
std::optional<TypeId> retType = first(functionType->retTypes);
REQUIRE(retType);
@ -1830,4 +1830,18 @@ TEST_CASE_FIXTURE(Fixture, "other_things_are_not_related_to_function")
CHECK(5 == result.errors[3].location.begin.line);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_must_follow_in_overload_resolution")
{
ScopedFastFlag luauTypeInferMissingFollows{"LuauTypeInferMissingFollows", true};
CheckResult result = check(R"(
for _ in function<t0>():(t0)&((()->())&(()->()))
end do
_(_(_,_,_),_)
end
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -10,6 +10,7 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauTypeMismatchInvarianceInError)
using namespace Luau;
@ -717,12 +718,24 @@ y.a.c = y
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]),
R"(Type 'y' could not be converted into 'T<string>'
if (FFlag::LuauTypeMismatchInvarianceInError)
{
CHECK_EQ(toString(result.errors[0]),
R"(Type 'y' could not be converted into 'T<string>'
caused by:
Property 'a' is not compatible. Type '{ c: T<string>?, d: number }' could not be converted into 'U<string>'
caused by:
Property 'd' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)");
}
else
{
CHECK_EQ(toString(result.errors[0]),
R"(Type 'y' could not be converted into 'T<string>'
caused by:
Property 'a' is not compatible. Type '{ c: T<string>?, d: number }' could not be converted into 'U<string>'
caused by:
Property 'd' is not compatible. Type 'number' could not be converted into 'string')");
}
}
TEST_CASE_FIXTURE(Fixture, "generic_type_pack_unification1")
@ -1249,4 +1262,21 @@ instantiate(function(x: string) return "foo" end)
CHECK_EQ("<a>(string) -> string", toString(tm1->givenType));
}
TEST_CASE_FIXTURE(Fixture, "bidirectional_checking_and_generalization_play_nice")
{
CheckResult result = check(R"(
local foo = function(a)
return a()
end
local a = foo(function() return 1 end)
local b = foo(function() return "bar" end)
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK("number" == toString(requireType("a")));
CHECK("string" == toString(requireType("b")));
}
TEST_SUITE_END();

View File

@ -185,7 +185,15 @@ TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_works_at_arbitrary_dep
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("string & string", toString(requireType("r")));
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("string", toString(requireType("r")));
}
else
{
CHECK_EQ("string & string", toString(requireType("r")));
}
}
TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_mixed_types")
@ -199,7 +207,7 @@ TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_mixed_types")
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("number & string", toString(requireType("r"))); // TODO(amccord): This should be an error.
CHECK_EQ("number & string", toString(requireType("r")));
}
TEST_CASE_FIXTURE(Fixture, "index_on_an_intersection_type_with_one_part_missing_the_property")
@ -525,18 +533,16 @@ TEST_CASE_FIXTURE(Fixture, "intersection_of_tables_with_never_properties")
ScopedFastFlag sffs[]{
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
{"LuauUninhabitedSubAnything", true},
};
CheckResult result = check(R"(
local x : { p : number?, q : never } & { p : never, q : string? }
local x : { p : number?, q : never } & { p : never, q : string? } -- OK
local y : { p : never, q : never } = x -- OK
local z : never = x -- OK
)");
// TODO: this should not produce type errors, since never <: { p : never }
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), "Type '{| p: never, q: string? |} & {| p: number?, q: never |}' could not be converted into 'never'; none "
"of the intersection parts are compatible");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "overloaded_functions_returning_intersections")
@ -848,7 +854,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatables_with_properties")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatable_with table")
TEST_CASE_FIXTURE(BuiltinsFixture, "intersect_metatable_with_table")
{
ScopedFastFlag sffs[]{
{"LuauSubtypeNormalizer", true},
@ -902,4 +908,37 @@ TEST_CASE_FIXTURE(Fixture, "CLI-44817")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_intersection_types")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local function f(t): { x: number } & { x: string }
local x = t.x
return t
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("({| x: number |} & {| x: string |}) -> {| x: number |} & {| x: string |}", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_intersection_types_2")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local function f(t: { x: number } & { x: string })
return t.x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("({| x: number |} & {| x: string |}) -> number & string", toString(requireType("f")));
}
TEST_SUITE_END();

View File

@ -11,6 +11,7 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauTypeMismatchInvarianceInError)
using namespace Luau;
@ -408,7 +409,12 @@ local b: B.T = a
CheckResult result = frontend.check("game/C");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'
if (FFlag::LuauTypeMismatchInvarianceInError)
CHECK_EQ(toString(result.errors[0]), R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'
caused by:
Property 'x' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)");
else
CHECK_EQ(toString(result.errors[0]), R"(Type 'T' from 'game/A' could not be converted into 'T' from 'game/B'
caused by:
Property 'x' is not compatible. Type 'number' could not be converted into 'string')");
}
@ -442,7 +448,12 @@ local b: B.T = a
CheckResult result = frontend.check("game/D");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'
if (FFlag::LuauTypeMismatchInvarianceInError)
CHECK_EQ(toString(result.errors[0]), R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'
caused by:
Property 'x' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)");
else
CHECK_EQ(toString(result.errors[0]), R"(Type 'T' from 'game/B' could not be converted into 'T' from 'game/C'
caused by:
Property 'x' is not compatible. Type 'number' could not be converted into 'string')");
}
@ -462,4 +473,15 @@ return l0
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_anyify_variadic_return_must_follow")
{
ScopedFastFlag luauTypeInferMissingFollows{"LuauTypeInferMissingFollows", true};
CheckResult result = check(R"(
return unpack(l0[_])
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -8,6 +8,7 @@
#include "Luau/VisitTypeVar.h"
#include "Fixture.h"
#include "ClassFixture.h"
#include "doctest.h"
@ -817,6 +818,21 @@ TEST_CASE_FIXTURE(Fixture, "operator_eq_operands_are_not_subtypes_of_each_other_
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "operator_eq_completely_incompatible")
{
ScopedFastFlag sff{"LuauIntersectionTestForEquality", true};
CheckResult result = check(R"(
local a: string | number = "hi"
local b: {x: string}? = {x = "bye"}
local r1 = a == b
local r2 = b == a
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
}
TEST_CASE_FIXTURE(Fixture, "refine_and_or")
{
CheckResult result = check(R"(
@ -916,6 +932,31 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "expected_types_through_binary_or")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(ClassFixture, "unrelated_classes_cannot_be_compared")
{
ScopedFastFlag sff{"LuauIntersectionTestForEquality", true};
CheckResult result = check(R"(
local a = BaseClass.New()
local b = UnrelatedClass.New()
local c = a == b
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(Fixture, "unrelated_primitives_cannot_be_compared")
{
ScopedFastFlag sff{"LuauIntersectionTestForEquality", true};
CheckResult result = check(R"(
local c = 5 == true
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "mm_ops_must_return_a_value")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)

View File

@ -170,24 +170,12 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "error_on_eq_metamethod_returning_a_type_othe
CHECK_EQ("Metamethod '__eq' must return type 'boolean'", ge->message);
}
// Requires success typing to confidently determine that this expression has no overlap.
TEST_CASE_FIXTURE(Fixture, "operator_eq_completely_incompatible")
{
CheckResult result = check(R"(
local a: string | number = "hi"
local b: {x: string}? = {x = "bye"}
local r1 = a == b
local r2 = b == a
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
// Belongs in TypeInfer.refinements.test.cpp.
// We'll need to not only report an error on `a == b`, but also to refine both operands as `never` in the `==` branch.
// We need refine both operands as `never` in the `==` branch.
TEST_CASE_FIXTURE(Fixture, "lvalue_equals_another_lvalue_with_no_overlap")
{
ScopedFastFlag sff{"LuauIntersectionTestForEquality", true};
CheckResult result = check(R"(
local function f(a: string, b: boolean?)
if a == b then
@ -198,7 +186,7 @@ TEST_CASE_FIXTURE(Fixture, "lvalue_equals_another_lvalue_with_no_overlap")
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "string"); // a == b
CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "boolean?"); // a == b

View File

@ -8,6 +8,7 @@
#include "doctest.h"
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution)
LUAU_FASTFLAG(LuauTypeMismatchInvarianceInError)
using namespace Luau;
@ -35,6 +36,27 @@ std::optional<WithPredicate<TypePackId>> magicFunctionInstanceIsA(
return WithPredicate<TypePackId>{booleanPack, {IsAPredicate{std::move(*lvalue), expr.location, tfun->type}}};
}
std::vector<ConnectiveId> dcrMagicRefinementInstanceIsA(MagicRefinementContext ctx)
{
if (ctx.callSite->args.size != 1)
return {};
auto index = ctx.callSite->func->as<Luau::AstExprIndexName>();
auto str = ctx.callSite->args.data[0]->as<Luau::AstExprConstantString>();
if (!index || !str)
return {};
std::optional<DefId> def = ctx.dfg->getDef(index->expr);
if (!def)
return {};
std::optional<TypeFun> tfun = ctx.scope->lookupType(std::string(str->value.data, str->value.size));
if (!tfun)
return {};
return {ctx.connectiveArena->proposition(*def, tfun->type)};
}
struct RefinementClassFixture : BuiltinsFixture
{
RefinementClassFixture()
@ -56,6 +78,7 @@ struct RefinementClassFixture : BuiltinsFixture
TypePackId isARets = arena.addTypePack({typeChecker.booleanType});
TypeId isA = arena.addType(FunctionTypeVar{isAParams, isARets});
getMutable<FunctionTypeVar>(isA)->magicFunction = magicFunctionInstanceIsA;
getMutable<FunctionTypeVar>(isA)->dcrMagicRefinement = dcrMagicRefinementInstanceIsA;
getMutable<ClassTypeVar>(inst)->props = {
{"Name", Property{typeChecker.stringType}},
@ -397,13 +420,21 @@ TEST_CASE_FIXTURE(Fixture, "truthy_constraint_on_properties")
local t: {x: number?} = {x = 1}
if t.x then
local foo: number = t.x
local t2 = t
local foo = t.x
end
local bar = t.x
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK("{| x: number? |} & {| x: ~(false?) |}" == toString(requireTypeAtPosition({4, 23})));
CHECK("(number?) & ~(false?)" == toString(requireTypeAtPosition({5, 26})));
}
CHECK_EQ("number?", toString(requireType("bar")));
}
@ -442,12 +473,24 @@ TEST_CASE_FIXTURE(Fixture, "assign_table_with_refined_property_with_a_similar_ty
)");
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK_EQ(R"(Type '{| x: number? |}' could not be converted into '{| x: number |}'
if (FFlag::LuauTypeMismatchInvarianceInError)
{
CHECK_EQ(R"(Type '{| x: number? |}' could not be converted into '{| x: number |}'
caused by:
Property 'x' is not compatible. Type 'number?' could not be converted into 'number' in an invariant context)",
toString(result.errors[0]));
}
else
{
CHECK_EQ(R"(Type '{| x: number? |}' could not be converted into '{| x: number |}'
caused by:
Property 'x' is not compatible. Type 'number?' could not be converted into 'number')",
toString(result.errors[0]));
toString(result.errors[0]));
}
}
TEST_CASE_FIXTURE(Fixture, "lvalue_is_equal_to_another_lvalue")
{
CheckResult result = check(R"(
@ -464,8 +507,8 @@ TEST_CASE_FIXTURE(Fixture, "lvalue_is_equal_to_another_lvalue")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "((number | string)?) & (boolean?)"); // a == b
CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "((number | string)?) & (boolean?)"); // a == b
CHECK_EQ(toString(requireTypeAtPosition({3, 33})), "((number | string)?) & unknown"); // a == b
CHECK_EQ(toString(requireTypeAtPosition({3, 36})), "(boolean?) & unknown"); // a == b
CHECK_EQ(toString(requireTypeAtPosition({5, 33})), "((number | string)?) & unknown"); // a ~= b
CHECK_EQ(toString(requireTypeAtPosition({5, 36})), "(boolean?) & unknown"); // a ~= b
@ -496,7 +539,7 @@ TEST_CASE_FIXTURE(Fixture, "lvalue_is_equal_to_a_term")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ(toString(requireTypeAtPosition({3, 28})), "((number | string)?) & number"); // a == 1
CHECK_EQ(toString(requireTypeAtPosition({3, 28})), "((number | string)?) & unknown"); // a == 1
CHECK_EQ(toString(requireTypeAtPosition({5, 28})), "((number | string)?) & unknown"); // a ~= 1
}
else
@ -548,8 +591,8 @@ TEST_CASE_FIXTURE(Fixture, "lvalue_is_not_nil")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ(toString(requireTypeAtPosition({3, 28})), "((number | string)?) & ~nil"); // a ~= nil
CHECK_EQ(toString(requireTypeAtPosition({5, 28})), "((number | string)?) & nil"); // a == nil
CHECK_EQ(toString(requireTypeAtPosition({3, 28})), "((number | string)?) & ~nil"); // a ~= nil
CHECK_EQ(toString(requireTypeAtPosition({5, 28})), "((number | string)?) & unknown"); // a == nil
}
else
{
@ -573,8 +616,8 @@ TEST_CASE_FIXTURE(Fixture, "free_type_is_equal_to_an_lvalue")
if (FFlag::DebugLuauDeferredConstraintResolution)
{
ToStringOptions opts;
CHECK_EQ(toString(requireTypeAtPosition({3, 33}), opts), "(string?) & a"); // a == b
CHECK_EQ(toString(requireTypeAtPosition({3, 36}), opts), "(string?) & a"); // a == b
CHECK_EQ(toString(requireTypeAtPosition({3, 33}), opts), "a & unknown"); // a == b
CHECK_EQ(toString(requireTypeAtPosition({3, 36}), opts), "(string?) & unknown"); // a == b
}
else
{
@ -628,8 +671,8 @@ TEST_CASE_FIXTURE(Fixture, "string_not_equal_to_string_or_nil")
CHECK_EQ(toString(requireTypeAtPosition({6, 29})), "string & unknown"); // a ~= b
CHECK_EQ(toString(requireTypeAtPosition({6, 32})), "(string?) & unknown"); // a ~= b
CHECK_EQ(toString(requireTypeAtPosition({8, 29})), "(string?) & string"); // a == b
CHECK_EQ(toString(requireTypeAtPosition({8, 32})), "(string?) & string"); // a == b
CHECK_EQ(toString(requireTypeAtPosition({8, 29})), "string & unknown"); // a == b
CHECK_EQ(toString(requireTypeAtPosition({8, 32})), "(string?) & unknown"); // a == b
}
else
{
@ -1146,8 +1189,17 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_from_truthiness_of_x")
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"({| tag: "exists", x: string |})", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ(R"({| tag: "exists", x: string |} | {| tag: "missing", x: nil |})", toString(requireTypeAtPosition({7, 28})));
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ(R"(({| tag: "exists", x: string |} | {| tag: "missing", x: nil |}) & {| x: ~(false?) |})", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ(
R"(({| tag: "exists", x: string |} | {| tag: "missing", x: nil |}) & {| x: ~~(false?) |})", toString(requireTypeAtPosition({7, 28})));
}
else
{
CHECK_EQ(R"({| tag: "exists", x: string |})", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ(R"({| tag: "exists", x: string |} | {| tag: "missing", x: nil |})", toString(requireTypeAtPosition({7, 28})));
}
}
TEST_CASE_FIXTURE(Fixture, "discriminate_tag")
@ -1159,17 +1211,57 @@ TEST_CASE_FIXTURE(Fixture, "discriminate_tag")
local function f(animal: Animal)
if animal.tag == "Cat" then
local cat: Cat = animal
local cat = animal
elseif animal.tag == "Dog" then
local dog: Dog = animal
local dog = animal
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("Cat", toString(requireTypeAtPosition({7, 33})));
CHECK_EQ("Dog", toString(requireTypeAtPosition({9, 33})));
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ(R"((Cat | Dog) & {| tag: "Cat" |})", toString(requireTypeAtPosition({7, 33})));
CHECK_EQ(R"((Cat | Dog) & {| tag: ~"Cat" |} & {| tag: "Dog" |})", toString(requireTypeAtPosition({9, 33})));
}
else
{
CHECK_EQ("Cat", toString(requireTypeAtPosition({7, 33})));
CHECK_EQ("Dog", toString(requireTypeAtPosition({9, 33})));
}
}
TEST_CASE_FIXTURE(Fixture, "discriminate_tag_with_implicit_else")
{
ScopedFastFlag sff{"LuauImplicitElseRefinement", true};
CheckResult result = check(R"(
type Cat = {tag: "Cat", name: string, catfood: string}
type Dog = {tag: "Dog", name: string, dogfood: string}
type Animal = Cat | Dog
local function f(animal: Animal)
if animal.tag == "Cat" then
local cat = animal
else
local dog = animal
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ(R"((Cat | Dog) & {| tag: "Cat" |})", toString(requireTypeAtPosition({7, 33})));
CHECK_EQ(R"((Cat | Dog) & {| tag: ~"Cat" |})", toString(requireTypeAtPosition({9, 33})));
}
else
{
CHECK_EQ("Cat", toString(requireTypeAtPosition({7, 33})));
CHECK_EQ("Dog", toString(requireTypeAtPosition({9, 33})));
}
}
TEST_CASE_FIXTURE(Fixture, "and_or_peephole_refinement")
@ -1258,8 +1350,16 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "discriminate_from_isa_of_x")
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ(R"({| tag: "Part", x: Part |})", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ(R"({| tag: "Folder", x: Folder |})", toString(requireTypeAtPosition({7, 28})));
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ(R"(({| tag: "Folder", x: Folder |} | {| tag: "Part", x: Part |}) & {| x: Part |})", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ(R"(({| tag: "Folder", x: Folder |} | {| tag: "Part", x: Part |}) & {| x: ~Part |})", toString(requireTypeAtPosition({7, 28})));
}
else
{
CHECK_EQ(R"({| tag: "Part", x: Part |})", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ(R"({| tag: "Folder", x: Folder |})", toString(requireTypeAtPosition({7, 28})));
}
}
TEST_CASE_FIXTURE(RefinementClassFixture, "typeguard_cast_free_table_to_vector")
@ -1399,8 +1499,96 @@ TEST_CASE_FIXTURE(RefinementClassFixture, "x_as_any_if_x_is_instance_elseif_x_is
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("Folder", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ("any", toString(requireTypeAtPosition({7, 28})));
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("Folder & Instance & {- -}", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ("(~Folder | ~Instance) & {- -} & never", toString(requireTypeAtPosition({7, 28})));
}
else
{
CHECK_EQ("Folder", toString(requireTypeAtPosition({5, 28})));
CHECK_EQ("any", toString(requireTypeAtPosition({7, 28})));
}
}
TEST_CASE_FIXTURE(RefinementClassFixture, "refine_param_of_type_instance_without_using_typeof")
{
CheckResult result = check(R"(
local function f(x: Instance)
if x:IsA("Folder") then
local foo = x
elseif typeof(x) == "table" then
local foo = x
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("Folder & Instance", toString(requireTypeAtPosition({3, 28})));
CHECK_EQ("Instance & ~Folder & never", toString(requireTypeAtPosition({5, 28})));
}
else
{
CHECK_EQ("Folder", toString(requireTypeAtPosition({3, 28})));
CHECK_EQ("never", toString(requireTypeAtPosition({5, 28})));
}
}
TEST_CASE_FIXTURE(RefinementClassFixture, "refine_param_of_type_folder_or_part_without_using_typeof")
{
CheckResult result = check(R"(
local function f(x: Part | Folder)
if x:IsA("Folder") then
local foo = x
else
local foo = x
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("(Folder | Part) & Folder", toString(requireTypeAtPosition({3, 28})));
CHECK_EQ("(Folder | Part) & ~Folder", toString(requireTypeAtPosition({5, 28})));
}
else
{
CHECK_EQ("Folder", toString(requireTypeAtPosition({3, 28})));
CHECK_EQ("Part", toString(requireTypeAtPosition({5, 28})));
}
}
TEST_CASE_FIXTURE(RefinementClassFixture, "isa_type_refinement_must_be_known_ahead_of_time")
{
CheckResult result = check(R"(
local function f(x): Instance
if x:IsA("Folder") then
local foo = x
else
local foo = x
end
return x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
if (FFlag::DebugLuauDeferredConstraintResolution)
{
CHECK_EQ("Instance", toString(requireTypeAtPosition({3, 28})));
CHECK_EQ("Instance", toString(requireTypeAtPosition({5, 28})));
}
else
{
CHECK_EQ("Instance", toString(requireTypeAtPosition({3, 28})));
CHECK_EQ("Instance", toString(requireTypeAtPosition({5, 28})));
}
}
TEST_CASE_FIXTURE(RefinementClassFixture, "x_is_not_instance_or_else_not_part")
@ -1526,4 +1714,18 @@ TEST_CASE_FIXTURE(Fixture, "else_with_no_explicit_expression_should_also_refine_
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "fuzz_filtered_refined_types_are_followed")
{
ScopedFastFlag luauTypeInferMissingFollows{"LuauTypeInferMissingFollows", true};
CheckResult result = check(R"(
local _
do
local _ = _ ~= _ or _ or _
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -18,6 +18,7 @@ LUAU_FASTFLAG(LuauLowerBoundsCalculation);
LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution);
LUAU_FASTFLAG(LuauInstantiateInSubtyping)
LUAU_FASTFLAG(LuauNoMoreGlobalSingletonTypes)
LUAU_FASTFLAG(LuauTypeMismatchInvarianceInError)
TEST_SUITE_BEGIN("TableTests");
@ -2024,7 +2025,12 @@ local b: B = a
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
if (FFlag::LuauTypeMismatchInvarianceInError)
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
caused by:
Property 'y' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)");
else
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
caused by:
Property 'y' is not compatible. Type 'number' could not be converted into 'string')");
}
@ -2043,7 +2049,14 @@ local b: B = a
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
if (FFlag::LuauTypeMismatchInvarianceInError)
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
caused by:
Property 'b' is not compatible. Type 'AS' could not be converted into 'BS'
caused by:
Property 'y' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)");
else
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
caused by:
Property 'b' is not compatible. Type 'AS' could not be converted into 'BS'
caused by:
@ -2063,7 +2076,14 @@ local c2: typeof(a2) = b2
)");
LUAU_REQUIRE_ERROR_COUNT(2, result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'b1' could not be converted into 'a1'
if (FFlag::LuauTypeMismatchInvarianceInError)
CHECK_EQ(toString(result.errors[0]), R"(Type 'b1' could not be converted into 'a1'
caused by:
Type '{ x: number, y: string }' could not be converted into '{ x: number, y: number }'
caused by:
Property 'y' is not compatible. Type 'string' could not be converted into 'number' in an invariant context)");
else
CHECK_EQ(toString(result.errors[0]), R"(Type 'b1' could not be converted into 'a1'
caused by:
Type '{ x: number, y: string }' could not be converted into '{ x: number, y: number }'
caused by:
@ -2098,7 +2118,12 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_key")
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
if (FFlag::LuauTypeMismatchInvarianceInError)
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
caused by:
Property '[indexer key]' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)");
else
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
caused by:
Property '[indexer key]' is not compatible. Type 'number' could not be converted into 'string')");
}
@ -2114,7 +2139,12 @@ TEST_CASE_FIXTURE(Fixture, "error_detailed_indexer_value")
)");
LUAU_REQUIRE_ERRORS(result);
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
if (FFlag::LuauTypeMismatchInvarianceInError)
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
caused by:
Property '[indexer value]' is not compatible. Type 'number' could not be converted into 'string' in an invariant context)");
else
CHECK_EQ(toString(result.errors[0]), R"(Type 'A' could not be converted into 'B'
caused by:
Property '[indexer value]' is not compatible. Type 'number' could not be converted into 'string')");
}
@ -3261,7 +3291,7 @@ caused by:
TEST_CASE_FIXTURE(Fixture, "a_free_shape_can_turn_into_a_scalar_if_it_is_compatible")
{
ScopedFastFlag sff{"LuauScalarShapeSubtyping", true};
ScopedFastFlag luauScalarShapeUnifyToMtOwner{"LuauScalarShapeUnifyToMtOwner", true}; // Changes argument from table type to primitive
ScopedFastFlag luauScalarShapeUnifyToMtOwner{"LuauScalarShapeUnifyToMtOwner2", true}; // Changes argument from table type to primitive
CheckResult result = check(R"(
local function f(s): string
@ -3308,7 +3338,7 @@ caused by:
TEST_CASE_FIXTURE(BuiltinsFixture, "a_free_shape_can_turn_into_a_scalar_directly")
{
ScopedFastFlag luauScalarShapeSubtyping{"LuauScalarShapeSubtyping", true};
ScopedFastFlag luauScalarShapeUnifyToMtOwner{"LuauScalarShapeUnifyToMtOwner", true};
ScopedFastFlag luauScalarShapeUnifyToMtOwner{"LuauScalarShapeUnifyToMtOwner2", true};
CheckResult result = check(R"(
local function stringByteList(str)
@ -3394,7 +3424,7 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "setmetatable_has_a_side_effect")
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK(toString(requireType("foo")) == "{ @metatable { __add: (a, b) -> number }, { } }");
CHECK(toString(requireType("foo")) == "{ @metatable { __add: <a, b>(a, b) -> number }, { } }");
}
TEST_CASE_FIXTURE(BuiltinsFixture, "tables_should_be_fully_populated")
@ -3413,4 +3443,43 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "tables_should_be_fully_populated")
CHECK_EQ("{ x: *error-type*, y: number }", toString(requireType("t"), opts));
}
TEST_CASE_FIXTURE(Fixture, "fuzz_table_indexer_unification_can_bound_owner_to_string")
{
ScopedFastFlag luauScalarShapeUnifyToMtOwner{"LuauScalarShapeUnifyToMtOwner2", true};
CheckResult result = check(R"(
sin,_ = nil
_ = _[_.sin][_._][_][_]._
_[_] = _
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_table_extra_prop_unification_can_bound_owner_to_string")
{
ScopedFastFlag luauScalarShapeUnifyToMtOwner{"LuauScalarShapeUnifyToMtOwner2", true};
CheckResult result = check(R"(
l0,_ = nil
_ = _,_[_.n5]._[_][_][_]._
_._.foreach[_],_ = _[_],_._
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_typelevel_promote_on_changed_table_type")
{
ScopedFastFlag luauScalarShapeUnifyToMtOwner{"LuauScalarShapeUnifyToMtOwner2", true};
CheckResult result = check(R"(
_._,_ = nil
_ = _.foreach[_]._,_[_.n5]._[_.foreach][_][_]._
_ = _._
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -351,7 +351,7 @@ TEST_CASE_FIXTURE(Fixture, "check_expr_recursion_limit")
CheckResult result = check(R"(("foo"))" + rep(":lower()", limit));
LUAU_REQUIRE_ERROR_COUNT(1, result);
CHECK(nullptr != get<CodeTooComplex>(result.errors[0]));
CHECK_MESSAGE(nullptr != get<CodeTooComplex>(result.errors[0]), "Expected CodeTooComplex but got " << toString(result.errors[0]));
}
TEST_CASE_FIXTURE(Fixture, "globals")
@ -1159,4 +1159,17 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "it_is_ok_to_have_inconsistent_number_of_retu
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "fuzz_free_table_type_change_during_index_check")
{
ScopedFastFlag luauFollowInLvalueIndexCheck{"LuauFollowInLvalueIndexCheck", true};
CheckResult result = check(R"(
local _ = nil
while _["" >= _] do
end
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -110,6 +110,68 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "incompatible_tables_are_preserved")
CHECK_NE(*getMutable<TableTypeVar>(&tableOne)->props["foo"].type, *getMutable<TableTypeVar>(&tableTwo)->props["foo"].type);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_intersection_sub_never")
{
ScopedFastFlag sffs[]{
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
function f(arg : string & number) : never
return arg
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_intersection_sub_anything")
{
ScopedFastFlag sffs[]{
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
};
CheckResult result = check(R"(
function f(arg : string & number) : boolean
return arg
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_never")
{
ScopedFastFlag sffs[]{
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
{"LuauUninhabitedSubAnything", true},
};
CheckResult result = check(R"(
function f(arg : { prop : string & number }) : never
return arg
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "uninhabited_table_sub_anything")
{
ScopedFastFlag sffs[]{
{"LuauSubtypeNormalizer", true},
{"LuauTypeNormalization2", true},
{"LuauUninhabitedSubAnything", true},
};
CheckResult result = check(R"(
function f(arg : { prop : string & number }) : boolean
return arg
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "members_of_failed_typepack_unification_are_unified_with_errorType")
{
CheckResult result = check(R"(
@ -299,4 +361,19 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "metatables_unify_against_shape_of_free_table
CHECK_EQ(toString(state.errors[0]), expected);
}
TEST_CASE_FIXTURE(TryUnifyFixture, "fuzz_tail_unification_issue")
{
ScopedFastFlag luauTxnLogTypePackIterator{"LuauTxnLogTypePackIterator", true};
TypePackVar variadicAny{VariadicTypePack{typeChecker.anyType}};
TypePackVar packTmp{TypePack{{typeChecker.anyType}, &variadicAny}};
TypePackVar packSub{TypePack{{typeChecker.anyType, typeChecker.anyType}, &packTmp}};
TypeVar freeTy{FreeTypeVar{TypeLevel{}}};
TypePackVar freeTp{FreeTypePack{TypeLevel{}}};
TypePackVar packSuper{TypePack{{&freeTy}, &freeTp}};
state.tryUnify(&packSub, &packSuper);
}
TEST_SUITE_END();

View File

@ -7,8 +7,6 @@
#include "doctest.h"
LUAU_FASTFLAG(LuauFunctionReturnStringificationFixup);
using namespace Luau;
TEST_SUITE_BEGIN("TypePackTests");
@ -311,10 +309,7 @@ local c: Packed<string, number, boolean>
auto ttvA = get<TableTypeVar>(requireType("a"));
REQUIRE(ttvA);
CHECK_EQ(toString(requireType("a")), "Packed<number>");
if (FFlag::LuauFunctionReturnStringificationFixup)
CHECK_EQ(toString(requireType("a"), {true}), "{| f: (number) -> number |}");
else
CHECK_EQ(toString(requireType("a"), {true}), "{| f: (number) -> (number) |}");
CHECK_EQ(toString(requireType("a"), {true}), "{| f: (number) -> number |}");
REQUIRE(ttvA->instantiatedTypeParams.size() == 1);
REQUIRE(ttvA->instantiatedTypePackParams.size() == 1);
CHECK_EQ(toString(ttvA->instantiatedTypeParams[0], {true}), "number");
@ -467,8 +462,6 @@ type I<S..., R...> = W<number, (string, S...), R...>
TEST_CASE_FIXTURE(Fixture, "type_alias_type_pack_explicit")
{
ScopedFastFlag sff("LuauFunctionReturnStringificationFixup", true);
CheckResult result = check(R"(
type X<T...> = (T...) -> (T...)
@ -492,8 +485,6 @@ type F = X<(string, ...number)>
TEST_CASE_FIXTURE(Fixture, "type_alias_type_pack_explicit_multi")
{
ScopedFastFlag sff("LuauFunctionReturnStringificationFixup", true);
CheckResult result = check(R"(
type Y<T..., U...> = (T...) -> (U...)
@ -1002,6 +993,10 @@ TEST_CASE_FIXTURE(Fixture, "unify_variadic_tails_in_arguments_free")
TEST_CASE_FIXTURE(BuiltinsFixture, "type_packs_with_tails_in_vararg_adjustment")
{
std::optional<ScopedFastFlag> sff;
if (FFlag::DebugLuauDeferredConstraintResolution)
sff = {"LuauInstantiateInSubtyping", true};
CheckResult result = check(R"(
local function wrapReject<TArg, TResult>(fn: (self: any, ...TArg) -> ...TResult): (self: any, ...TArg) -> ...TResult
return function(self, ...)
@ -1017,4 +1012,46 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "type_packs_with_tails_in_vararg_adjustment")
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "generalize_expectedTypes_with_proper_scope")
{
ScopedFastFlag sff[] = {
{"DebugLuauDeferredConstraintResolution", true},
{"LuauInstantiateInSubtyping", true},
};
CheckResult result = check(R"(
local function f<TResult>(fn: () -> ...TResult): () -> ...TResult
return function()
end
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_CASE_FIXTURE(Fixture, "fuzz_typepack_iter_follow")
{
ScopedFastFlag luauTxnLogTypePackIterator{"LuauTxnLogTypePackIterator", true};
CheckResult result = check(R"(
local _
local _ = _,_(),_(_)
)");
LUAU_REQUIRE_ERRORS(result);
}
TEST_CASE_FIXTURE(BuiltinsFixture, "fuzz_typepack_iter_follow_2")
{
ScopedFastFlag luauTxnLogTypePackIterator{"LuauTxnLogTypePackIterator", true};
CheckResult result = check(R"(
function test(name, searchTerm)
local found = string.find(name:lower(), searchTerm:lower())
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
}
TEST_SUITE_END();

View File

@ -729,4 +729,37 @@ TEST_CASE_FIXTURE(Fixture, "union_of_functions_with_mismatching_result_variadics
"of the union options are compatible");
}
TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local function f(t): { x: number } | { x: string }
local x = t.x
return t
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("({| x: number |} | {| x: string |}) -> {| x: number |} | {| x: string |}", toString(requireType("f")));
}
TEST_CASE_FIXTURE(Fixture, "less_greedy_unification_with_union_types_2")
{
if (!FFlag::DebugLuauDeferredConstraintResolution)
return;
CheckResult result = check(R"(
local function f(t: { x: number } | { x: string })
return t.x
end
)");
LUAU_REQUIRE_NO_ERRORS(result);
CHECK_EQ("({| x: number |} | {| x: string |}) -> number | string", toString(requireType("f")));
}
TEST_SUITE_END();

View File

@ -22,14 +22,7 @@ TEST_CASE_FIXTURE(Fixture, "throw_when_limit_is_exceeded")
TypeId tType = requireType("t");
if (FFlag::LuauIceExceptionInheritanceChange)
{
CHECK_THROWS_AS(toString(tType), RecursionLimitException);
}
else
{
CHECK_THROWS_AS(toString(tType), RecursionLimitException_DEPRECATED);
}
CHECK_THROWS_AS(toString(tType), RecursionLimitException);
}
TEST_CASE_FIXTURE(Fixture, "dont_throw_when_limit_is_high_enough")

View File

@ -14,9 +14,11 @@ AstQuery::getDocumentationSymbolAtPosition.overloaded_class_method
AstQuery::getDocumentationSymbolAtPosition.overloaded_fn
AstQuery::getDocumentationSymbolAtPosition.table_overloaded_function_prop
AutocompleteTest.autocomplete_first_function_arg_expected_type
AutocompleteTest.autocomplete_interpolated_string
AutocompleteTest.autocomplete_interpolated_string_as_singleton
AutocompleteTest.autocomplete_interpolated_string_constant
AutocompleteTest.autocomplete_interpolated_string_expression
AutocompleteTest.autocomplete_interpolated_string_expression_with_comments
AutocompleteTest.autocomplete_oop_implicit_self
AutocompleteTest.autocomplete_string_singleton_equality
AutocompleteTest.autocomplete_string_singleton_escape
AutocompleteTest.autocomplete_string_singletons
AutocompleteTest.autocompleteProp_index_function_metamethod_is_variadic
@ -25,9 +27,11 @@ AutocompleteTest.do_wrong_compatible_self_calls
AutocompleteTest.keyword_methods
AutocompleteTest.no_incompatible_self_calls
AutocompleteTest.no_wrong_compatible_self_calls_with_generics
AutocompleteTest.suggest_external_module_type
AutocompleteTest.suggest_table_keys
AutocompleteTest.type_correct_argument_type_suggestion
AutocompleteTest.type_correct_expected_argument_type_pack_suggestion
AutocompleteTest.type_correct_expected_argument_type_suggestion
AutocompleteTest.type_correct_expected_argument_type_suggestion_optional
AutocompleteTest.type_correct_expected_argument_type_suggestion_self
AutocompleteTest.type_correct_expected_return_type_pack_suggestion
@ -68,7 +72,6 @@ BuiltinTests.select_with_decimal_argument_is_rounded_down
BuiltinTests.set_metatable_needs_arguments
BuiltinTests.setmetatable_should_not_mutate_persisted_types
BuiltinTests.sort_with_bad_predicate
BuiltinTests.string_format_arg_count_mismatch
BuiltinTests.string_format_as_method
BuiltinTests.string_format_correctly_ordered_types
BuiltinTests.string_format_report_all_type_errors_at_correct_positions
@ -106,10 +109,10 @@ GenericsTests.generic_factories
GenericsTests.generic_functions_should_be_memory_safe
GenericsTests.generic_table_method
GenericsTests.generic_type_pack_parentheses
GenericsTests.generic_type_pack_unification2
GenericsTests.higher_rank_polymorphism_should_not_accept_instantiated_arguments
GenericsTests.infer_generic_function_function_argument
GenericsTests.infer_generic_function_function_argument_overloaded
GenericsTests.infer_generic_lib_function_function_argument
GenericsTests.infer_generic_methods
GenericsTests.infer_generic_property
GenericsTests.instantiated_function_argument_names
@ -117,9 +120,6 @@ GenericsTests.instantiation_sharing_types
GenericsTests.no_stack_overflow_from_quantifying
GenericsTests.reject_clashing_generic_and_pack_names
GenericsTests.self_recursive_instantiated_param
IntersectionTypes.index_on_an_intersection_type_with_mixed_types
IntersectionTypes.index_on_an_intersection_type_with_property_guaranteed_to_exist
IntersectionTypes.index_on_an_intersection_type_works_at_arbitrary_depth
IntersectionTypes.no_stack_overflow_from_flattenintersection
IntersectionTypes.select_correct_union_fn
IntersectionTypes.should_still_pick_an_overload_whose_arguments_are_unions
@ -151,6 +151,7 @@ ProvisionalTests.bail_early_if_unification_is_too_complicated
ProvisionalTests.discriminate_from_x_not_equal_to_nil
ProvisionalTests.do_not_ice_when_trying_to_pick_first_of_generic_type_pack
ProvisionalTests.error_on_eq_metamethod_returning_a_type_other_than_boolean
ProvisionalTests.free_options_cannot_be_unified_together
ProvisionalTests.generic_type_leak_to_module_interface_variadic
ProvisionalTests.greedy_inference_with_shared_self_triggers_function_with_no_returns
ProvisionalTests.lvalue_equals_another_lvalue_with_no_overlap
@ -164,15 +165,15 @@ ProvisionalTests.while_body_are_also_refined
RefinementTest.apply_refinements_on_astexprindexexpr_whose_subscript_expr_is_constant_string
RefinementTest.assert_a_to_be_truthy_then_assert_a_to_be_number
RefinementTest.assert_non_binary_expressions_actually_resolve_constraints
RefinementTest.assign_table_with_refined_property_with_a_similar_type_is_illegal
RefinementTest.call_an_incompatible_function_after_using_typeguard
RefinementTest.correctly_lookup_property_whose_base_was_previously_refined
RefinementTest.correctly_lookup_property_whose_base_was_previously_refined2
RefinementTest.discriminate_from_isa_of_x
RefinementTest.discriminate_from_truthiness_of_x
RefinementTest.discriminate_on_properties_of_disjoint_tables_where_that_property_is_true_or_false
RefinementTest.discriminate_tag
RefinementTest.else_with_no_explicit_expression_should_also_refine_the_tagged_union
RefinementTest.falsiness_of_TruthyPredicate_narrows_into_nil
RefinementTest.fuzz_filtered_refined_types_are_followed
RefinementTest.index_on_a_refined_property
RefinementTest.invert_is_truthy_constraint_ifelse_expression
RefinementTest.is_truthy_constraint_ifelse_expression
@ -181,7 +182,6 @@ RefinementTest.nonoptional_type_can_narrow_to_nil_if_sense_is_true
RefinementTest.not_t_or_some_prop_of_t
RefinementTest.refine_a_property_not_to_be_nil_through_an_intersection_table
RefinementTest.refine_unknowns
RefinementTest.truthy_constraint_on_properties
RefinementTest.type_comparison_ifelse_expression
RefinementTest.type_guard_can_filter_for_intersection_of_tables
RefinementTest.type_guard_narrowed_into_nothingness
@ -210,7 +210,6 @@ TableTests.defining_a_self_method_for_a_builtin_sealed_table_must_fail
TableTests.defining_a_self_method_for_a_local_sealed_table_must_fail
TableTests.dont_crash_when_setmetatable_does_not_produce_a_metatabletypevar
TableTests.dont_hang_when_trying_to_look_up_in_cyclic_metatable_index
TableTests.dont_leak_free_table_props
TableTests.dont_quantify_table_that_belongs_to_outer_scope
TableTests.dont_suggest_exact_match_keys
TableTests.error_detailed_metatable_prop
@ -240,6 +239,7 @@ TableTests.invariant_table_properties_means_instantiating_tables_in_assignment_i
TableTests.invariant_table_properties_means_instantiating_tables_in_call_is_unsound
TableTests.leaking_bad_metatable_errors
TableTests.less_exponential_blowup_please
TableTests.meta_add
TableTests.meta_add_both_ways
TableTests.meta_add_inferred
TableTests.metatable_mismatch_should_fail
@ -252,8 +252,6 @@ TableTests.only_ascribe_synthetic_names_at_module_scope
TableTests.oop_indexer_works
TableTests.oop_polymorphic
TableTests.open_table_unification_2
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table
TableTests.pass_a_union_of_tables_to_a_function_that_requires_a_table_2
TableTests.persistent_sealed_table_is_immutable
TableTests.prop_access_on_key_whose_types_mismatches
TableTests.property_lookup_through_tabletypevar_metatable
@ -268,6 +266,7 @@ TableTests.scalar_is_not_a_subtype_of_a_compatible_polymorphic_shape_type
TableTests.shared_selfs
TableTests.shared_selfs_from_free_param
TableTests.shared_selfs_through_metatables
TableTests.table_call_metamethod_basic
TableTests.table_indexing_error_location
TableTests.table_insert_should_cope_with_optional_properties_in_nonstrict
TableTests.table_insert_should_cope_with_optional_properties_in_strict
@ -323,6 +322,7 @@ TypeInfer.checking_should_not_ice
TypeInfer.cli_50041_committing_txnlog_in_apollo_client_error
TypeInfer.dont_report_type_errors_within_an_AstExprError
TypeInfer.dont_report_type_errors_within_an_AstStatError
TypeInfer.fuzz_free_table_type_change_during_index_check
TypeInfer.globals
TypeInfer.globals2
TypeInfer.infer_assignment_value_types_mutable_lval
@ -335,7 +335,6 @@ TypeInfer.tc_interpolated_string_with_invalid_expression
TypeInfer.type_infer_recursion_limit_no_ice
TypeInfer.type_infer_recursion_limit_normalizer
TypeInferAnyError.for_in_loop_iterator_is_any2
TypeInferAnyError.for_in_loop_iterator_is_error2
TypeInferClasses.can_read_prop_of_base_class_using_string
TypeInferClasses.class_type_mismatch_with_name_conflict
TypeInferClasses.classes_without_overloaded_operators_cannot_be_added
@ -351,8 +350,6 @@ TypeInferFunctions.cannot_hoist_interior_defns_into_signature
TypeInferFunctions.dont_give_other_overloads_message_if_only_one_argument_matching_overload_exists
TypeInferFunctions.dont_infer_parameter_types_for_functions_from_their_call_site
TypeInferFunctions.duplicate_functions_with_different_signatures_not_allowed_in_nonstrict
TypeInferFunctions.free_is_not_bound_to_unknown
TypeInferFunctions.func_expr_doesnt_leak_free
TypeInferFunctions.function_cast_error_uses_correct_language
TypeInferFunctions.function_decl_non_self_sealed_overwrite_2
TypeInferFunctions.function_decl_non_self_unsealed_overwrite
@ -385,12 +382,11 @@ TypeInferLoops.for_in_loop
TypeInferLoops.for_in_loop_error_on_factory_not_returning_the_right_amount_of_values
TypeInferLoops.for_in_loop_with_next
TypeInferLoops.for_in_with_generic_next
TypeInferLoops.for_in_with_just_one_iterator_is_ok
TypeInferLoops.loop_iter_metamethod_ok_with_inference
TypeInferLoops.loop_iter_no_indexer_nonstrict
TypeInferLoops.loop_iter_trailing_nil
TypeInferLoops.properly_infer_iteratee_is_a_free_table
TypeInferLoops.unreachable_code_after_infinite_loop
TypeInferLoops.varlist_declared_by_for_in_loop_should_be_free
TypeInferModules.custom_require_global
TypeInferModules.do_not_modify_imported_types
TypeInferModules.module_type_conflict
@ -414,6 +410,8 @@ TypeInferOperators.compound_assign_mismatch_result
TypeInferOperators.disallow_string_and_types_without_metatables_from_arithmetic_binary_ops
TypeInferOperators.in_nonstrict_mode_strip_nil_from_intersections_when_considering_relational_operators
TypeInferOperators.infer_any_in_all_modes_when_lhs_is_unknown
TypeInferOperators.mm_comparisons_must_return_a_boolean
TypeInferOperators.mm_ops_must_return_a_value
TypeInferOperators.produce_the_correct_error_message_when_comparing_a_table_with_a_metatable_with_one_that_does_not
TypeInferOperators.refine_and_or
TypeInferOperators.typecheck_overloaded_multiply_that_is_an_intersection
@ -427,16 +425,13 @@ TypeInferUnknownNever.assign_to_prop_which_is_never
TypeInferUnknownNever.assign_to_subscript_which_is_never
TypeInferUnknownNever.call_never
TypeInferUnknownNever.dont_unify_operands_if_one_of_the_operand_is_never_in_any_ordering_operators
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_never
TypeInferUnknownNever.index_on_union_of_tables_for_properties_that_is_sorta_never
TypeInferUnknownNever.math_operators_and_never
TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable
TypeInferUnknownNever.type_packs_containing_never_is_itself_uninhabitable2
TypeInferUnknownNever.unary_minus_of_never
TypePackTests.detect_cyclic_typepacks2
TypePackTests.higher_order_function
TypePackTests.pack_tail_unification_check
TypePackTests.parenthesized_varargs_returns_any
TypePackTests.type_alias_backwards_compatible
TypePackTests.type_alias_default_export
TypePackTests.type_alias_default_mixed_self
@ -456,7 +451,6 @@ TypePackTests.type_alias_type_packs_nested
TypePackTests.type_pack_type_parameters
TypePackTests.unify_variadic_tails_in_arguments
TypePackTests.unify_variadic_tails_in_arguments_free
TypePackTests.varargs_inference_through_multiple_scopes
TypePackTests.variadic_packs
TypeSingletons.error_detailed_tagged_union_mismatch_bool
TypeSingletons.error_detailed_tagged_union_mismatch_string
@ -477,11 +471,8 @@ TypeSingletons.widening_happens_almost_everywhere_except_for_tables
UnionTypes.error_detailed_optional
UnionTypes.error_detailed_union_all
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.optional_assignment_errors
UnionTypes.optional_call_error
UnionTypes.optional_field_access_error