diff --git a/Analysis/include/Luau/BuiltinDefinitions.h b/Analysis/include/Luau/BuiltinDefinitions.h index 616367b..4702995 100644 --- a/Analysis/include/Luau/BuiltinDefinitions.h +++ b/Analysis/include/Luau/BuiltinDefinitions.h @@ -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 documentationSymbol = std::nullopt); void assignPropDocumentationSymbols(TableTypeVar::Props& props, const std::string& baseName); diff --git a/Analysis/include/Luau/Connective.h b/Analysis/include/Luau/Connective.h index c9daa0f..4a6be93 100644 --- a/Analysis/include/Luau/Connective.h +++ b/Analysis/include/Luau/Connective.h @@ -3,7 +3,6 @@ #include "Luau/Def.h" #include "Luau/TypedAllocator.h" -#include "Luau/TypeVar.h" #include "Luau/Variant.h" #include @@ -11,6 +10,9 @@ namespace Luau { +struct TypeVar; +using TypeId = const TypeVar*; + struct Negation; struct Conjunction; struct Disjunction; diff --git a/Analysis/include/Luau/Constraint.h b/Analysis/include/Luau/Constraint.h index 16a08e8..e13613e 100644 --- a/Analysis/include/Luau/Constraint.h +++ b/Analysis/include/Luau/Constraint.h @@ -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 expectedType = {}); /** * Checks the body of a function expression. diff --git a/Analysis/include/Luau/DataFlowGraphBuilder.h b/Analysis/include/Luau/DataFlowGraph.h similarity index 91% rename from Analysis/include/Luau/DataFlowGraphBuilder.h rename to Analysis/include/Luau/DataFlowGraph.h index 3a72403..bd096ea 100644 --- a/Analysis/include/Luau/DataFlowGraphBuilder.h +++ b/Analysis/include/Luau/DataFlowGraph.h @@ -69,9 +69,14 @@ private: struct InternalErrorReporter* handle; std::vector> 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> props{nullptr}; + DfgScope* childScope(DfgScope* scope); std::optional 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); diff --git a/Analysis/include/Luau/Def.h b/Analysis/include/Luau/Def.h index ac1fa13..1eef7df 100644 --- a/Analysis/include/Luau/Def.h +++ b/Analysis/include/Luau/Def.h @@ -5,37 +5,38 @@ #include "Luau/TypedAllocator.h" #include "Luau/Variant.h" +#include +#include + namespace Luau { -using Def = Variant; - -/** - * 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; +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 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 operands; }; -template -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(def.get()); -} + using V = Variant; + + V v; +}; template const T* get(DefId def) { - return getMutable(def); + return get_if(&def->v); } struct DefArena { TypedAllocator 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& defs); }; } // namespace Luau diff --git a/Analysis/include/Luau/Error.h b/Analysis/include/Luau/Error.h index f7bd9d5..8938804 100644 --- a/Analysis/include/Luau/Error.h +++ b/Analysis/include/Luau/Error.h @@ -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 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 error, Context context); + TypeId wantedType = nullptr; TypeId givenType = nullptr; + Context context = CovariantContext; std::string reason; std::shared_ptr 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; + 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 @@ -406,10 +440,4 @@ public: const std::optional 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 diff --git a/Analysis/include/Luau/Normalize.h b/Analysis/include/Luau/Normalize.h index b28c06a..d7e104e 100644 --- a/Analysis/include/Luau/Normalize.h +++ b/Analysis/include/Luau/Normalize.h @@ -194,6 +194,8 @@ struct NormalizedFunctionType struct NormalizedType; using NormalizedTyvars = std::unordered_map>; +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 seen = {}); + bool isInhabited(const NormalizedType* norm, std::unordered_set seen = {}); + // -------- Convert back from a normalized type to a type TypeId typeFromNormal(const NormalizedType& norm); }; diff --git a/Analysis/include/Luau/NotNull.h b/Analysis/include/Luau/NotNull.h index 714fa14..ecdcb47 100644 --- a/Analysis/include/Luau/NotNull.h +++ b/Analysis/include/Luau/NotNull.h @@ -59,6 +59,20 @@ struct NotNull return ptr; } + template + bool operator==(NotNull other) const noexcept + { + return get() == other.get(); + } + + template + bool operator!=(NotNull other) const noexcept + { + return get() != other.get(); + } + + operator bool() const noexcept = delete; + T& operator[](int) = delete; T& operator+(int) = delete; diff --git a/Analysis/include/Luau/RecursionCounter.h b/Analysis/include/Luau/RecursionCounter.h index 632afd1..77af10a 100644 --- a/Analysis/include/Luau/RecursionCounter.h +++ b/Analysis/include/Luau/RecursionCounter.h @@ -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(); } } }; diff --git a/Analysis/include/Luau/ToString.h b/Analysis/include/Luau/ToString.h index 0200a71..186cc9a 100644 --- a/Analysis/include/Luau/ToString.h +++ b/Analysis/include/Luau/ToString.h @@ -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); diff --git a/Analysis/include/Luau/TxnLog.h b/Analysis/include/Luau/TxnLog.h index 3c3122c..b1a8341 100644 --- a/Analysis/include/Luau/TxnLog.h +++ b/Analysis/include/Luau/TxnLog.h @@ -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 arena); + void concatAsUnion(TxnLog rhs, NotNull arena); // Commits the TxnLog, rebinding all type pointers to their pending states. // Clears the TxnLog afterwards. diff --git a/Analysis/include/Luau/TypeInfer.h b/Analysis/include/Luau/TypeInfer.h index 2eea191..c6f153d 100644 --- a/Analysis/include/Luau/TypeInfer.h +++ b/Analysis/include/Luau/TypeInfer.h @@ -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 diff --git a/Analysis/include/Luau/TypeUtils.h b/Analysis/include/Luau/TypeUtils.h index 085ee21..aa9cdde 100644 --- a/Analysis/include/Luau/TypeUtils.h +++ b/Analysis/include/Luau/TypeUtils.h @@ -25,9 +25,9 @@ std::optional getIndexTypeFromType(const ScopePtr& scope, ErrorVec& erro // Returns the minimum and maximum number of types the argument list can accept. std::pair> 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 flatten(TypeArena& arena, NotNull 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, TypePackId pack, size_t length); /** * Reduces a union by decomposing to the any/error type if it appears in the diff --git a/Analysis/include/Luau/TypeVar.h b/Analysis/include/Luau/TypeVar.h index 0ab4d47..d355746 100644 --- a/Analysis/include/Luau/TypeVar.h +++ b/Analysis/include/Luau/TypeVar.h @@ -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; +using DcrMagicFunction = bool (*)(MagicFunctionCallContext); + +struct MagicRefinementContext +{ + ScopePtr scope; + NotNull dfg; + NotNull connectiveArena; + const class AstExprCall* callSite; +}; + +using DcrMagicRefinement = std::vector (*)(MagicRefinementContext); struct FunctionTypeVar { @@ -279,19 +291,20 @@ struct FunctionTypeVar FunctionTypeVar(TypeLevel level, Scope* scope, std::vector generics, std::vector genericPacks, TypePackId argTypes, TypePackId retTypes, std::optional defn = {}, bool hasSelf = false); - TypeLevel level; - Scope* scope = nullptr; + std::optional definition; /// These should all be generic std::vector generics; std::vector genericPacks; - TypePackId argTypes; std::vector> argNames; - TypePackId retTypes; - std::optional 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; }; diff --git a/Analysis/include/Luau/Unifier.h b/Analysis/include/Luau/Unifier.h index b5f58d3..af3864e 100644 --- a/Analysis/include/Luau/Unifier.h +++ b/Analysis/include/Luau/Unifier.h @@ -120,6 +120,9 @@ private: std::optional findTablePropertyRespectingMeta(TypeId lhsType, Name name); + TxnLog combineLogsIntoIntersection(std::vector logs); + TxnLog combineLogsIntoUnion(std::vector 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); diff --git a/Analysis/src/AstQuery.cpp b/Analysis/src/AstQuery.cpp index b93c2cc..85d2320 100644 --- a/Analysis/src/AstQuery.cpp +++ b/Analysis/src/AstQuery.cpp @@ -11,15 +11,12 @@ #include -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 checkOverloadedDocumentationSymbol( const Module& module, const TypeId ty, const AstExpr* parentExpr, const std::optional documentationSymbol) { - LUAU_ASSERT(FFlag::LuauCheckOverloadedDocSymbol); - if (!documentationSymbol) return std::nullopt; @@ -469,40 +464,7 @@ std::optional getDocumentationSymbolAtPosition(const Source AstExpr* parentExpr = ancestry.size() >= 2 ? ancestry[ancestry.size() - 2]->asExpr() : nullptr; if (std::optional 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(follow(binding->typeId))) - { - TypeId matchingOverload = nullptr; - if (parentExpr && parentExpr->is()) - { - 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 getDocumentationSymbolAtPosition(const Source if (const TableTypeVar* ttv = get(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(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); } } } diff --git a/Analysis/src/Autocomplete.cpp b/Analysis/src/Autocomplete.cpp index 50dc254..5374c6b 100644 --- a/Analysis/src/Autocomplete.cpp +++ b/Analysis/src/Autocomplete.cpp @@ -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(ancestry); statRepeat) return {autocompleteStatement(sourceModule, *module, ancestry, position), ancestry, AutocompleteContext::Statement}; - else if (AstExprTable* exprTable = parent->as(); exprTable && (node->is() || node->is() || node->is())) + else if (AstExprTable* exprTable = parent->as(); + exprTable && (node->is() || node->is() || node->is())) { for (const auto& [kind, key, value] : exprTable->items) { diff --git a/Analysis/src/BuiltinDefinitions.cpp b/Analysis/src/BuiltinDefinitions.cpp index 67e3979..3956867 100644 --- a/Analysis/src/BuiltinDefinitions.cpp +++ b/Analysis/src/BuiltinDefinitions.cpp @@ -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(ty)) + ftv->dcrMagicRefinement = fn; + else + LUAU_ASSERT(!"Got a non functional type"); +} + Property makeProperty(TypeId ty, std::optional documentationSymbol) { return { diff --git a/Analysis/src/ConstraintGraphBuilder.cpp b/Analysis/src/ConstraintGraphBuilder.cpp index e3572fe..d41c777 100644 --- a/Analysis/src/ConstraintGraphBuilder.cpp +++ b/Analysis/src/ConstraintGraphBuilder.cpp @@ -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& lhs, const } static void computeRefinement(const ScopePtr& scope, ConnectiveId connective, std::unordered_map* refis, bool sense, - NotNull arena, bool eq, std::vector* constraints) + NotNull arena, bool eq, std::vector* constraints) { using RefinementMap = std::unordered_map; @@ -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 computeDiscriminantType(NotNull arena, const ScopePtr& scope, DefId def, TypeId discriminantTy) +{ + LUAU_ASSERT(get(def)); + + while (const Cell* current = get(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(def); + } + + return {def, discriminantTy}; +} + void ConstraintGraphBuilder::applyRefinements(const ScopePtr& scope, Location location, ConnectiveId connective) { if (!connective) return; std::unordered_map refinements; - std::vector constraints; + std::vector constraints; computeRefinement(scope, connective, &refinements, /*sense*/ true, arena, /*eq*/ false, &constraints); for (auto [def, discriminantTy] : refinements) { - std::optional defTy = scope->lookup(def); + auto [def2, discriminantTy2] = computeDiscriminantType(arena, scope, def, discriminantTy); + std::optional 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 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 constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()}; std::unique_ptr 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 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 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()) { std::optional 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()) { 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 prospectiveTable{getMutable(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()) { - 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 constraintScope{sig.signatureScope ? sig.signatureScope.get() : sig.bodyScope.get()}; std::unique_ptr c = - std::make_unique(constraintScope, function->name->location, GeneralizationConstraint{functionType, sig.signature}); + std::make_unique(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()) - result = {checkPack(scope, call, expectedTypes)}; + result = checkPack(scope, call, expectedTypes); else if (AstExprVarargs* varargs = expr->as()) { 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& expectedTypes) { - TypeId fnType = check(scope, call->func).ty; - auto startCheckpoint = checkpoint(this); - - std::vector args; - - for (AstExpr* arg : call->args) - { - args.push_back(check(scope, arg).ty); - } - + std::vector exprArgs; if (call->self) { AstExprIndexName* indexExpr = call->func->as(); 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 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() || lastArg->is()); + + TypePack expectedArgs; + + if (!needTail) + expectedArgs = extendTypePack(*arena, singletonTypes, expectedArgPack, exprArgs.size()); + else + expectedArgs = extendTypePack(*arena, singletonTypes, expectedArgPack, exprArgs.size() - 1); + + std::vector connectives; + if (auto ftv = get(follow(fnType)); ftv && ftv->dcrMagicRefinement) + { + MagicRefinementContext ctx{globalScope, dfg, NotNull{&connectiveArena}, call}; + connectives = ftv->dcrMagicRefinement(ctx); + } + + + std::vector args; + std::optional argTail; + + Checkpoint argCheckpoint = checkpoint(this); + + for (size_t i = 0; i < exprArgs.size(); ++i) + { + AstExpr* arg = exprArgs[i]; + std::optional 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() || arg->is())) + 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()) 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 ic(unqueuedConstraints.back().get()); unqueuedConstraints.push_back( - std::make_unique(NotNull{scope.get()}, call->func->location, SubtypeConstraint{inferredFnType, instantiatedType})); + std::make_unique(NotNull{scope.get()}, call->func->location, SubtypeConstraint{instantiatedType, inferredFnType})); NotNull 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 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()) { - 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 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()) result = check(scope, indexName); @@ -1253,10 +1343,83 @@ Inference ConstraintGraphBuilder::check(const ScopePtr& scope, AstExprGlobal* gl return Inference{singletonTypes->errorRecoveryType()}; } +static std::optional lookupProp(TypeId ty, const std::string& propName, NotNull arena) +{ + ty = follow(ty); + + if (auto ctv = get(ty)) + { + if (auto prop = lookupClassProp(ctv, propName)) + return prop->type; + } + else if (auto ttv = get(ty)) + { + if (auto it = ttv->props.find(propName); it != ttv->props.end()) + return it->second.type; + } + else if (auto utv = get(ty)) + { + std::vector 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(ty)) + { + std::vector 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 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 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()) { - expectedValueType = arena->addType(BlockedTypeVar{}); - addConstraint(scope, item.value->location, HasPropConstraint{*expectedValueType, *expectedType, stringKey->value.data}); + ErrorVec errorVec; + std::optional 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 expectedType) { ScopePtr signatureScope = nullptr; ScopePtr bodyScope = nullptr; @@ -1599,22 +1774,22 @@ ConstraintGraphBuilder::FunctionSignature ConstraintGraphBuilder::checkFunctionS std::vector genericTypes; std::vector 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> genericDefinitions = createGenerics(signatureScope, fn->generics); std::vector> 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: may be used uninitialized + std::optional none = std::nullopt; + expectedType = none; } - else + + std::vector argTypes; + TypePack expectedArgPack; + + const FunctionTypeVar* expectedFunction = expectedType ? get(*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(*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(returnType)); asMutable(returnType)->ty.emplace(annotatedRetType); } - - std::vector 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(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(*expectedType)) + { + asMutable(*expectedType)->ty.emplace(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, }; } diff --git a/Analysis/src/ConstraintSolver.cpp b/Analysis/src/ConstraintSolver.cpp index 250e7ae..d59ea70 100644 --- a/Analysis/src/ConstraintSolver.cpp +++ b/Analysis/src/ConstraintSolver.cpp @@ -1233,9 +1233,22 @@ bool ConstraintSolver::tryDispatch(const HasPropConstraint& c, NotNull(subjectType)) return block(subjectType, constraint); + if (get(subjectType)) + { + TableTypeVar& ttv = asMutable(subjectType)->ty.emplace(TableState::Free, TypeLevel{}, constraint->scope); + ttv.props[c.prop] = Property{c.resultType}; + asMutable(c.resultType)->ty.emplace(constraint->scope); + unblock(c.resultType); + return true; + } + std::optional resultType = lookupTableProp(subjectType, c.prop); if (!resultType) - return false; + { + asMutable(c.resultType)->ty.emplace(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(followed) || isNil(followed)) + if (c.negated && (get(followed) || isNil(followed))) *asMutable(c.resultType) = NegationTypeVar{c.discriminantType}; + else if (!c.negated && get(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 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 instantiatedNextFn = instantiation.substitute(nextFn)) { diff --git a/Analysis/src/DataFlowGraphBuilder.cpp b/Analysis/src/DataFlowGraph.cpp similarity index 92% rename from Analysis/src/DataFlowGraphBuilder.cpp rename to Analysis/src/DataFlowGraph.cpp index e2c4c28..cffd00c 100644 --- a/Analysis/src/DataFlowGraphBuilder.cpp +++ b/Analysis/src/DataFlowGraph.cpp @@ -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 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()) + expr = group->expr; if (auto def = astDefs.find(expr)) return NotNull{*def}; return std::nullopt; @@ -52,16 +55,25 @@ std::optional 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()) { - 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; } diff --git a/Analysis/src/Def.cpp b/Analysis/src/Def.cpp index 935301c..8ce1129 100644 --- a/Analysis/src/Def.cpp +++ b/Analysis/src/Def.cpp @@ -4,9 +4,14 @@ namespace Luau { -DefId DefArena::freshDef() +DefId DefArena::freshCell() { - return NotNull{allocator.allocate(Undefined{})}; + return NotNull{allocator.allocate(Def{Cell{std::nullopt}})}; +} + +DefId DefArena::freshCell(DefId parent, const std::string& prop) +{ + return NotNull{allocator.allocate(Def{Cell{FieldMetadata{parent, prop}}})}; } } // namespace Luau diff --git a/Analysis/src/Error.cpp b/Analysis/src/Error.cpp index ed1a49c..aefaa2c 100644 --- a/Analysis/src/Error.cpp +++ b/Analysis/src/Error.cpp @@ -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 +#include -LUAU_FASTFLAGVARIABLE(LuauIceExceptionInheritanceChange, false) +LUAU_FASTFLAGVARIABLE(LuauTypeMismatchInvarianceInError, false) static std::string wrongNumberOfArgsString( size_t expectedCount, std::optional 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 error, TypeMismatch::Context context) + : wantedType(wantedType) + , givenType(givenType) + , context(context) + , reason(reason) + , error(error ? std::make_shared(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) + e.ty = clone(e.ty); else static_assert(always_false_v, "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 diff --git a/Analysis/src/Frontend.cpp b/Analysis/src/Frontend.cpp index 22a9ecf..356ced0 100644 --- a/Analysis/src/Frontend.cpp +++ b/Analysis/src/Frontend.cpp @@ -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 typesToPersist; + typesToPersist.reserve(checkedModule->declaredGlobals.size() + checkedModule->getModuleScope()->exportedTypeBindings.size()); + + for (const auto& [name, ty] : checkedModule->declaredGlobals) { - std::vector 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 typesToPersist; + typesToPersist.reserve(checkedModule->declaredGlobals.size() + checkedModule->getModuleScope()->exportedTypeBindings.size()); + + for (const auto& [name, ty] : checkedModule->declaredGlobals) { - std::vector 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::optionalsecond == 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) stream << "TypePackMismatch { wanted = '" + toString(err.wantedTp) + "', given = '" + toString(err.givenTp) + "' }"; + else if constexpr (std::is_same_v) + stream << "DynamicPropertyLookupOnClassesUnsafe { " << toString(err.ty) << " }"; else static_assert(always_false_v, "Non-exhaustive type switch"); } diff --git a/Analysis/src/Normalize.cpp b/Analysis/src/Normalize.cpp index 21e9f78..fa3503f 100644 --- a/Analysis/src/Normalize.cpp +++ b/Analysis/src/Normalize.cpp @@ -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) { } -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(norm.tops) || !get(norm.booleans) || !norm.classes.empty() || !get(norm.errors) || !get(norm.nils) || !get(norm.numbers) || !norm.strings.isNever() || !get(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 seen) +{ + if (!get(norm->tops) || !get(norm->booleans) || !get(norm->errors) || + !get(norm->nils) || !get(norm->numbers) || !get(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 seen) +{ + // TODO: use log.follow(ty), CLI-64291 + ty = follow(ty); + + if (get(ty)) + return false; + + if (!get(ty) && !get(ty) && !get(ty) && !get(ty)) + return true; + + if (seen.count(ty)) + return true; + + seen.insert(ty); + + if (const TableTypeVar* ttv = get(ty)) + { + for (const auto& [_, prop] : ttv->props) + { + if (!isInhabited(prop.type, seen)) + return false; + } + return true; + } + + if (const MetatableTypeVar* mtv = get(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(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); diff --git a/Analysis/src/ToString.cpp b/Analysis/src/ToString.cpp index fe5f569..ddcc15a 100644 --- a/Analysis/src/ToString.cpp +++ b/Analysis/src/ToString.cpp @@ -11,8 +11,8 @@ #include 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(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> 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; - 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) { - 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) { @@ -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, "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(*current)) - s = "." + field->key + s; - else if (auto symbol = get(*current)) - s = toString(*symbol) + s; - else - LUAU_ASSERT(!"Unknown LValue"); - } - - return s; -} - std::optional getFunctionNameAsString(const AstExpr& expr) { - LUAU_ASSERT(FFlag::LuauLvaluelessPath); - const AstExpr* curr = &expr; std::string s; diff --git a/Analysis/src/TopoSortStatements.cpp b/Analysis/src/TopoSortStatements.cpp index 052c10d..fbf7411 100644 --- a/Analysis/src/TopoSortStatements.cpp +++ b/Analysis/src/TopoSortStatements.cpp @@ -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; diff --git a/Analysis/src/TxnLog.cpp b/Analysis/src/TxnLog.cpp index 034aeae..1a73b04 100644 --- a/Analysis/src/TxnLog.cpp +++ b/Analysis/src/TxnLog.cpp @@ -78,6 +78,42 @@ void TxnLog::concat(TxnLog rhs) typePackChanges[tp] = std::move(rep); } +void TxnLog::concatAsIntersections(TxnLog rhs, NotNull 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 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) diff --git a/Analysis/src/TypeAttach.cpp b/Analysis/src/TypeAttach.cpp index c97ed05..e483c04 100644 --- a/Analysis/src/TypeAttach.cpp +++ b/Analysis/src/TypeAttach.cpp @@ -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: diff --git a/Analysis/src/TypeChecker2.cpp b/Analysis/src/TypeChecker2.cpp index 35493bd..84c0ca3 100644 --- a/Analysis/src/TypeChecker2.cpp +++ b/Analysis/src/TypeChecker2.cpp @@ -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 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 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 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 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(iteratorTy)) { - checkFunction(nextFn, iteratorTypes, false); + checkFunction(nextFn, iteratorTypes.head, false); } else if (const TableTypeVar* ttv = get(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 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 instantiatedNextFn = instantiation.substitute(nextFn)) { - std::vector instantiatedIteratorTypes = mmIteratorTypes; + std::vector instantiatedIteratorTypes = mmIteratorTypes.head; instantiatedIteratorTypes[0] = *instantiatedNextFn; if (const FunctionTypeVar* nextFtv = get(*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(functionType)) + { + // Sometimes it's okay to call a union of functions, but only if all of the functions are the same. + std::optional 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 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(); 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 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(*mm)) + if (const FunctionTypeVar* ftv = get(follow(*mm))) { TypePackId expectedArgs; // For >= and > we invoke __lt and __le respectively with diff --git a/Analysis/src/TypeInfer.cpp b/Analysis/src/TypeInfer.cpp index dfd08e5..9f64a60 100644 --- a/Analysis/src/TypeInfer.cpp +++ b/Analysis/src/TypeInfer.cpp @@ -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 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 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 areEqComparable(NotNull arena, NotNull normalizer, TypeId a, TypeId b) +{ + a = follow(a); + b = follow(b); + + auto isExempt = [](TypeId t) { + return isNil(t) || get(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 eqTestResult = areEqComparable(NotNull{¤tModule->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(leftType) || get(leftType) || get(leftType) || get(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(exprType) || get(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(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 path = getFunctionNameAsString(funName)) - namePath = *path; - } - else - { - if (std::optional lValue = tryGetLValue(funName)) - namePath = toString(*lValue); - } + if (std::optional 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 path = getFunctionNameAsString(funName)) - namePath = *path; - } - else - { - if (std::optional lValue = tryGetLValue(funName)) - namePath = toString(*lValue); - } + if (std::optional 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(tp)) - return get(vtp->ty) ? anyTypePack : tp; + { + TypeId ty = FFlag::LuauTypeInferMissingFollows ? follow(vtp->ty) : vtp->ty; + return get(ty) ? anyTypePack : tp; + } if (!get(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 { diff --git a/Analysis/src/TypePack.cpp b/Analysis/src/TypePack.cpp index 0852f05..0f75c3e 100644 --- a/Analysis/src/TypePack.cpp +++ b/Analysis/src/TypePack.cpp @@ -6,6 +6,8 @@ #include +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(currentTypePack)) + : currentTypePack(FFlag::LuauTxnLogTypePackIterator ? log->follow(typePack) : follow(typePack)) + , tp(FFlag::LuauTxnLogTypePackIterator ? log->get(currentTypePack) : get(currentTypePack)) , currentIndex(0) , log(log) { @@ -235,7 +237,7 @@ TypePackId follow(TypePackId tp, std::function mapper) cycleTester = nullptr; if (tp == cycleTester) - throwRuntimeError("Luau::follow detected a TypeVar cycle!!"); + throw InternalCompilerError("Luau::follow detected a TypeVar cycle!!"); } } } diff --git a/Analysis/src/TypeUtils.cpp b/Analysis/src/TypeUtils.cpp index 72597c4..876c45a 100644 --- a/Analysis/src/TypeUtils.cpp +++ b/Analysis/src/TypeUtils.cpp @@ -122,10 +122,6 @@ std::optional 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(follow(t))) return t; @@ -164,9 +160,6 @@ std::optional 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 ty = getIndexTypeFromType(scope, errors, arena, singletonTypes, t, prop, location, /* addErrors= */ false, handle)) parts.push_back(*ty); @@ -183,7 +176,7 @@ std::optional 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> getParameterExtents(const TxnLog* log, return {minCount, minCount + optionalCount}; } -std::vector flatten(TypeArena& arena, NotNull singletonTypes, TypePackId pack, size_t length) +TypePack extendTypePack(TypeArena& arena, NotNull singletonTypes, TypePackId pack, size_t length) { - std::vector 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(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(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(pack)) + { + while (result.head.size() < length) + result.head.push_back(vtp->ty); + result.tail = pack; return result; + } + else if (FreeTypePack* ftp = getMutable(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(tail)) - LUAU_ASSERT(0); - else if (auto vtp = get(tail)) - { - while (result.size() < length) - result.push_back(vtp->ty); - } - else if (get(tail) || get(tail)) - { - while (result.size() < length) - result.push_back(arena.addType(FreeTypeVar{nullptr})); - } - else if (auto etp = get(tail)) - { - while (result.size() < length) - result.push_back(singletonTypes->errorRecoveryType()); - } + asMutable(pack)->ty.emplace(std::move(newPack)); - return result; + return result; + } + else if (const Unifiable::Error* etp = getMutable(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 reduceUnion(const std::vector& types) diff --git a/Analysis/src/TypeVar.cpp b/Analysis/src/TypeVar.cpp index 814eca0..6771d89 100644 --- a/Analysis/src/TypeVar.cpp +++ b/Analysis/src/TypeVar.cpp @@ -72,7 +72,7 @@ TypeId follow(TypeId t, std::function mapper) { TypeId res = ltv->thunk(); if (get(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 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 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 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 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 generics, std::vector genericPacks, TypePackId argTypes, TypePackId retTypes, std::optional 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 generics, std::vector genericPacks, TypePackId argTypes, TypePackId retTypes, std::optional 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 generics, std::vector genericPacks, TypePackId argTypes, TypePackId retTypes, std::optional 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) { } diff --git a/Analysis/src/TypedAllocator.cpp b/Analysis/src/TypedAllocator.cpp index 133104d..4dc2621 100644 --- a/Analysis/src/TypedAllocator.cpp +++ b/Analysis/src/TypedAllocator.cpp @@ -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); diff --git a/Analysis/src/Unifier.cpp b/Analysis/src/Unifier.cpp index 4dc9098..5ff405e 100644 --- a/Analysis/src/Unifier.cpp +++ b/Analysis/src/Unifier.cpp @@ -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 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(ty)) + return true; + promote(ty, log.getMutable(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(ty)) + return true; + promote(ty, log.getMutable(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, Mode mode, NotNull 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(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 unificationTooComplex; std::optional firstFailedOption; + std::vector 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(superOption); - if (!log.is(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(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(superOption)) - log.bindTable(superOption, subTy); - else - log.replace(superOption, *subTy); - } - }; - - if (auto superUnion = log.getMutable(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(superOption); + if (!log.is(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(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(superOption)) + log.bindTable(superOption, subTy); + else + log.replace(superOption, *subTy); + } + }; + + if (auto superUnion = log.getMutable(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 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 unificationTooComplex; std::optional firstFailedOption; + std::vector 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 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(superNorm.tops) || get(superNorm.tops) || get(subNorm.tops)) return; else if (get(subNorm.tops)) - return reportError(location, TypeMismatch{superTy, subTy, reason, error}); + return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()}); if (get(subNorm.errors)) if (!get(superNorm.errors)) - return reportError(location, TypeMismatch{superTy, subTy, reason, error}); + return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()}); if (get(subNorm.booleans)) { if (!get(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(subNorm.booleans)) { if (!get(superNorm.booleans) && stv != get(superNorm.booleans)) - return reportError(location, TypeMismatch{superTy, subTy, reason, error}); + return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()}); } if (get(subNorm.nils)) if (!get(superNorm.nils)) - return reportError(location, TypeMismatch{superTy, subTy, reason, error}); + return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()}); if (get(subNorm.numbers)) if (!get(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(subNorm.threads)) if (!get(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(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(log.follow(*superTpv->tail)) != nullptr; const bool rFreeTail = subTpv->tail && log.getMutable(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(superIter.packId)) + tryUnifyVariadics(subIter.packId, superIter.packId, false, int(subIter.index)); + else if (log.getMutable(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(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(superTy); - TableTypeVar* newSubTable = log.getMutable(subTy); + TableTypeVar* newSuperTable = log.getMutable(superTyNew); + TableTypeVar* newSubTable = log.getMutable(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(superTy); - subTable = log.getMutable(subTy); + if (FFlag::LuauScalarShapeUnifyToMtOwner2) + { + superTable = log.getMutable(log.follow(superTy)); + subTable = log.getMutable(log.follow(subTy)); + + if (!superTable || !subTable) + return; + } + else + { + superTable = log.getMutable(superTy); + subTable = log.getMutable(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(superTy); if (!superTable || superTable->state != TableState::Free) - return reportError(location, TypeMismatch{osuperTy, osubTy}); + return reportError(location, TypeMismatch{osuperTy, osubTy, mismatchContext()}); auto fail = [&](std::optional 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(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(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 & queue, DenseHashSet& 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(subTp)) + if (const VariadicTypePack* subVariadic = FFlag::LuauTxnLogTypePackIterator ? log.get(subTp) : get(subTp)) + { tryUnify_(reversed ? superVariadic->ty : subVariadic->ty, reversed ? subVariadic->ty : superVariadic->ty); - else if (get(subTp)) + } + else if (FFlag::LuauTxnLogTypePackIterator ? log.get(subTp) : get(subTp)) { TypePackIterator subIter = begin(subTp, &log); TypePackIterator subEnd = end(subTp); @@ -2350,6 +2464,24 @@ std::optional Unifier::findTablePropertyRespectingMeta(TypeId lhsType, N return Luau::findTablePropertyRespectingMeta(singletonTypes, errors, lhsType, name, location); } +TxnLog Unifier::combineLogsIntoIntersection(std::vector 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 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) diff --git a/Ast/include/Luau/Location.h b/Ast/include/Luau/Location.h index d3c0a46..e39bbf8 100644 --- a/Ast/include/Luau/Location.h +++ b/Ast/include/Luau/Location.h @@ -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); diff --git a/Ast/src/Parser.cpp b/Ast/src/Parser.cpp index 85b0d31..5cd5f74 100644 --- a/Ast/src/Parser.cpp +++ b/Ast/src/Parser.cpp @@ -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& 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& result, TempVector, AstArray> Parser::parseG { nextLexeme(); - if (FFlag::LuauCommaParenWarnings && lexer.current().type == '>') + if (lexer.current().type == '>') { report(lexer.current().location, "Expected type after ',' but got '>' instead"); break; diff --git a/Compiler/src/BytecodeBuilder.cpp b/Compiler/src/BytecodeBuilder.cpp index 8376424..7d23073 100644 --- a/Compiler/src/BytecodeBuilder.cpp +++ b/Compiler/src/BytecodeBuilder.cpp @@ -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; diff --git a/Compiler/src/Compiler.cpp b/Compiler/src/Compiler.cpp index 7ccd116..5d67236 100644 --- a/Compiler/src/Compiler.cpp +++ b/Compiler/src/Compiler.cpp @@ -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 diff --git a/Sources.cmake b/Sources.cmake index 33ecde9..e243ea7 100644 --- a/Sources.cmake +++ b/Sources.cmake @@ -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 diff --git a/VM/src/loslib.cpp b/VM/src/loslib.cpp index 91ccec0..62a5668 100644 --- a/VM/src/loslib.cpp +++ b/VM/src/loslib.cpp @@ -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) diff --git a/tests/AstQuery.test.cpp b/tests/AstQuery.test.cpp index 48bb40d..a642334 100644 --- a/tests/AstQuery.test.cpp +++ b/tests/AstQuery.test.cpp @@ -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) diff --git a/tests/ClassFixture.cpp b/tests/ClassFixture.cpp new file mode 100644 index 0000000..18939e2 --- /dev/null +++ b/tests/ClassFixture.cpp @@ -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(baseClassInstanceType)->props = { + {"BaseMethod", {makeFunction(arena, baseClassInstanceType, {numberType}, {})}}, + {"BaseField", {numberType}}, + }; + + TypeId baseClassType = arena.addType(ClassTypeVar{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test"}); + getMutable(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(childClassInstanceType)->props = { + {"Method", {makeFunction(arena, childClassInstanceType, {}, {typeChecker.stringType})}}, + }; + + TypeId childClassType = arena.addType(ClassTypeVar{"ChildClass", {}, baseClassType, nullopt, {}, {}, "Test"}); + getMutable(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(grandChildInstanceType)->props = { + {"Method", {makeFunction(arena, grandChildInstanceType, {}, {typeChecker.stringType})}}, + }; + + TypeId grandChildType = arena.addType(ClassTypeVar{"GrandChild", {}, baseClassType, nullopt, {}, {}, "Test"}); + getMutable(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(anotherChildInstanceType)->props = { + {"Method", {makeFunction(arena, anotherChildInstanceType, {}, {typeChecker.stringType})}}, + }; + + TypeId anotherChildType = arena.addType(ClassTypeVar{"AnotherChild", {}, baseClassType, nullopt, {}, {}, "Test"}); + getMutable(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(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(vector2InstanceType)->props = { + {"X", {numberType}}, + {"Y", {numberType}}, + }; + + TypeId vector2Type = arena.addType(ClassTypeVar{"Vector2", {}, nullopt, nullopt, {}, {}, "Test"}); + getMutable(vector2Type)->props = { + {"New", {makeFunction(arena, nullopt, {numberType, numberType}, {vector2InstanceType})}}, + }; + getMutable(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(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 diff --git a/tests/ClassFixture.h b/tests/ClassFixture.h new file mode 100644 index 0000000..66aec76 --- /dev/null +++ b/tests/ClassFixture.h @@ -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 diff --git a/tests/Compiler.test.cpp b/tests/Compiler.test.cpp index 3da40df..d2cf0ae 100644 --- a/tests/Compiler.test.cpp +++ b/tests/Compiler.test.cpp @@ -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 )"); } diff --git a/tests/DataFlowGraphBuilder.test.cpp b/tests/DataFlowGraph.test.cpp similarity index 98% rename from tests/DataFlowGraphBuilder.test.cpp rename to tests/DataFlowGraph.test.cpp index 9aa7cde..d823070 100644 --- a/tests/DataFlowGraphBuilder.test.cpp +++ b/tests/DataFlowGraph.test.cpp @@ -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" diff --git a/tests/Module.test.cpp b/tests/Module.test.cpp index b289b59..33d9c75 100644 --- a/tests/Module.test.cpp +++ b/tests/Module.test.cpp @@ -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") diff --git a/tests/NotNull.test.cpp b/tests/NotNull.test.cpp index dfa06aa..b827b81 100644 --- a/tests/NotNull.test.cpp +++ b/tests/NotNull.test.cpp @@ -9,6 +9,8 @@ using Luau::NotNull; +static_assert(!std::is_convertible, bool>::value, "NotNull ought not to be convertible into bool"); + namespace { diff --git a/tests/Parser.test.cpp b/tests/Parser.test.cpp index c0989a2..18e91e1 100644 --- a/tests/Parser.test.cpp +++ b/tests/Parser.test.cpp @@ -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: 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 diff --git a/tests/ToString.test.cpp b/tests/ToString.test.cpp index 8bb1fba..29954dc 100644 --- a/tests/ToString.test.cpp +++ b/tests/ToString.test.cpp @@ -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") diff --git a/tests/TypeInfer.aliases.test.cpp b/tests/TypeInfer.aliases.test.cpp index ef40e27..53c54f4 100644 --- a/tests/TypeInfer.aliases.test.cpp +++ b/tests/TypeInfer.aliases.test.cpp @@ -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'\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'\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'\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'\n" - "caused by:\n" - " Property 't' is not compatible. Type '{ v: string }' could not be converted into 'T'\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'\n" + "caused by:\n" + " Property 't' is not compatible. Type '{ v: string }' could not be converted into 'T'\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'\n" + "caused by:\n" + " Property 't' is not compatible. Type '{ v: string }' could not be converted into 'T'\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); diff --git a/tests/TypeInfer.annotations.test.cpp b/tests/TypeInfer.annotations.test.cpp index bb97bbe..b94e1df 100644 --- a/tests/TypeInfer.annotations.test.cpp +++ b/tests/TypeInfer.annotations.test.cpp @@ -7,8 +7,6 @@ #include "doctest.h" -LUAU_FASTFLAG(LuauIceExceptionInheritanceChange) - using namespace Luau; TEST_SUITE_BEGIN("AnnotationTests"); diff --git a/tests/TypeInfer.builtins.test.cpp b/tests/TypeInfer.builtins.test.cpp index 787aea9..32e31e1 100644 --- a/tests/TypeInfer.builtins.test.cpp +++ b/tests/TypeInfer.builtins.test.cpp @@ -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") diff --git a/tests/TypeInfer.classes.test.cpp b/tests/TypeInfer.classes.test.cpp index d1a24e5..07dfc33 100644 --- a/tests/TypeInfer.classes.test.cpp +++ b/tests/TypeInfer.classes.test.cpp @@ -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(baseClassInstanceType)->props = { - {"BaseMethod", {makeFunction(arena, baseClassInstanceType, {numberType}, {})}}, - {"BaseField", {numberType}}, - }; - - TypeId baseClassType = arena.addType(ClassTypeVar{"BaseClass", {}, nullopt, nullopt, {}, {}, "Test"}); - getMutable(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(childClassInstanceType)->props = { - {"Method", {makeFunction(arena, childClassInstanceType, {}, {typeChecker.stringType})}}, - }; - - TypeId childClassType = arena.addType(ClassTypeVar{"ChildClass", {}, baseClassType, nullopt, {}, {}, "Test"}); - getMutable(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(grandChildInstanceType)->props = { - {"Method", {makeFunction(arena, grandChildInstanceType, {}, {typeChecker.stringType})}}, - }; - - TypeId grandChildType = arena.addType(ClassTypeVar{"GrandChild", {}, baseClassType, nullopt, {}, {}, "Test"}); - getMutable(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(anotherChildInstanceType)->props = { - {"Method", {makeFunction(arena, anotherChildInstanceType, {}, {typeChecker.stringType})}}, - }; - - TypeId anotherChildType = arena.addType(ClassTypeVar{"AnotherChild", {}, baseClassType, nullopt, {}, {}, "Test"}); - getMutable(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(vector2InstanceType)->props = { - {"X", {numberType}}, - {"Y", {numberType}}, - }; - - TypeId vector2Type = arena.addType(ClassTypeVar{"Vector2", {}, nullopt, nullopt, {}, {}, "Test"}); - getMutable(vector2Type)->props = { - {"New", {makeFunction(arena, nullopt, {numberType, numberType}, {vector2InstanceType})}}, - }; - getMutable(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(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}; diff --git a/tests/TypeInfer.definitions.test.cpp b/tests/TypeInfer.definitions.test.cpp index 3556f0f..2611504 100644 --- a/tests/TypeInfer.definitions.test.cpp +++ b/tests/TypeInfer.definitions.test.cpp @@ -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) diff --git a/tests/TypeInfer.functions.test.cpp b/tests/TypeInfer.functions.test.cpp index b306515..5521804 100644 --- a/tests/TypeInfer.functions.test.cpp +++ b/tests/TypeInfer.functions.test.cpp @@ -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(requireType("most_of_the_natural_numbers")); + TypeId ty = requireType("most_of_the_natural_numbers"); + const FunctionTypeVar* functionType = get(ty); + REQUIRE_MESSAGE(functionType, "Expected function but got " << toString(ty)); std::optional 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)&((()->())&(()->())) +end do +_(_(_,_,_),_) +end + )"); + + LUAU_REQUIRE_ERRORS(result); +} + TEST_SUITE_END(); diff --git a/tests/TypeInfer.generics.test.cpp b/tests/TypeInfer.generics.test.cpp index de41c3a..c25f8e5 100644 --- a/tests/TypeInfer.generics.test.cpp +++ b/tests/TypeInfer.generics.test.cpp @@ -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' + if (FFlag::LuauTypeMismatchInvarianceInError) + { + CHECK_EQ(toString(result.errors[0]), + R"(Type 'y' could not be converted into 'T' +caused by: + Property 'a' is not compatible. Type '{ c: T?, d: number }' could not be converted into 'U' +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' caused by: Property 'a' is not compatible. Type '{ c: T?, d: number }' could not be converted into 'U' 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("(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(); diff --git a/tests/TypeInfer.intersectionTypes.test.cpp b/tests/TypeInfer.intersectionTypes.test.cpp index 0c10eb8..7d0621a 100644 --- a/tests/TypeInfer.intersectionTypes.test.cpp +++ b/tests/TypeInfer.intersectionTypes.test.cpp @@ -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(); diff --git a/tests/TypeInfer.modules.test.cpp b/tests/TypeInfer.modules.test.cpp index 4cc628f..b06c80e 100644 --- a/tests/TypeInfer.modules.test.cpp +++ b/tests/TypeInfer.modules.test.cpp @@ -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(); diff --git a/tests/TypeInfer.operators.test.cpp b/tests/TypeInfer.operators.test.cpp index c4bbc2e..2180608 100644 --- a/tests/TypeInfer.operators.test.cpp +++ b/tests/TypeInfer.operators.test.cpp @@ -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) diff --git a/tests/TypeInfer.provisional.test.cpp b/tests/TypeInfer.provisional.test.cpp index 5688eaa..2593417 100644 --- a/tests/TypeInfer.provisional.test.cpp +++ b/tests/TypeInfer.provisional.test.cpp @@ -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 diff --git a/tests/TypeInfer.refinements.test.cpp b/tests/TypeInfer.refinements.test.cpp index 66550be..e5bc186 100644 --- a/tests/TypeInfer.refinements.test.cpp +++ b/tests/TypeInfer.refinements.test.cpp @@ -8,6 +8,7 @@ #include "doctest.h" LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) +LUAU_FASTFLAG(LuauTypeMismatchInvarianceInError) using namespace Luau; @@ -35,6 +36,27 @@ std::optional> magicFunctionInstanceIsA( return WithPredicate{booleanPack, {IsAPredicate{std::move(*lvalue), expr.location, tfun->type}}}; } +std::vector dcrMagicRefinementInstanceIsA(MagicRefinementContext ctx) +{ + if (ctx.callSite->args.size != 1) + return {}; + + auto index = ctx.callSite->func->as(); + auto str = ctx.callSite->args.data[0]->as(); + if (!index || !str) + return {}; + + std::optional def = ctx.dfg->getDef(index->expr); + if (!def) + return {}; + + std::optional 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(isA)->magicFunction = magicFunctionInstanceIsA; + getMutable(isA)->dcrMagicRefinement = dcrMagicRefinementInstanceIsA; getMutable(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(); diff --git a/tests/TypeInfer.tables.test.cpp b/tests/TypeInfer.tables.test.cpp index 10186e3..c94ed1f 100644 --- a/tests/TypeInfer.tables.test.cpp +++ b/tests/TypeInfer.tables.test.cpp @@ -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) -> 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(); diff --git a/tests/TypeInfer.test.cpp b/tests/TypeInfer.test.cpp index 04b8bf5..e42cea6 100644 --- a/tests/TypeInfer.test.cpp +++ b/tests/TypeInfer.test.cpp @@ -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(result.errors[0])); + CHECK_MESSAGE(nullptr != get(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(); diff --git a/tests/TypeInfer.tryUnify.test.cpp b/tests/TypeInfer.tryUnify.test.cpp index f04a3d9..5cc07a2 100644 --- a/tests/TypeInfer.tryUnify.test.cpp +++ b/tests/TypeInfer.tryUnify.test.cpp @@ -110,6 +110,68 @@ TEST_CASE_FIXTURE(TryUnifyFixture, "incompatible_tables_are_preserved") CHECK_NE(*getMutable(&tableOne)->props["foo"].type, *getMutable(&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(); diff --git a/tests/TypeInfer.typePacks.cpp b/tests/TypeInfer.typePacks.cpp index cde651d..0e4074f 100644 --- a/tests/TypeInfer.typePacks.cpp +++ b/tests/TypeInfer.typePacks.cpp @@ -7,8 +7,6 @@ #include "doctest.h" -LUAU_FASTFLAG(LuauFunctionReturnStringificationFixup); - using namespace Luau; TEST_SUITE_BEGIN("TypePackTests"); @@ -311,10 +309,7 @@ local c: Packed auto ttvA = get(requireType("a")); REQUIRE(ttvA); CHECK_EQ(toString(requireType("a")), "Packed"); - 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 = W TEST_CASE_FIXTURE(Fixture, "type_alias_type_pack_explicit") { - ScopedFastFlag sff("LuauFunctionReturnStringificationFixup", true); - CheckResult result = check(R"( type X = (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...) @@ -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 sff; + if (FFlag::DebugLuauDeferredConstraintResolution) + sff = {"LuauInstantiateInSubtyping", true}; + CheckResult result = check(R"( local function wrapReject(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(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(); diff --git a/tests/TypeInfer.unionTypes.test.cpp b/tests/TypeInfer.unionTypes.test.cpp index 627fbb5..adfc61b 100644 --- a/tests/TypeInfer.unionTypes.test.cpp +++ b/tests/TypeInfer.unionTypes.test.cpp @@ -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(); diff --git a/tests/VisitTypeVar.test.cpp b/tests/VisitTypeVar.test.cpp index 589c3ba..4fba694 100644 --- a/tests/VisitTypeVar.test.cpp +++ b/tests/VisitTypeVar.test.cpp @@ -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") diff --git a/tools/faillist.txt b/tools/faillist.txt index 433d0cf..6f49db8 100644 --- a/tools/faillist.txt +++ b/tools/faillist.txt @@ -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