// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Unifier.h" #include "Luau/Common.h" #include "Luau/Instantiation.h" #include "Luau/RecursionCounter.h" #include "Luau/Scope.h" #include "Luau/StringUtils.h" #include "Luau/TimeTrace.h" #include "Luau/ToString.h" #include "Luau/TypePack.h" #include "Luau/TypeUtils.h" #include "Luau/Type.h" #include "Luau/VisitType.h" #include LUAU_FASTINT(LuauTypeInferTypePackLoopLimit) LUAU_FASTFLAG(LuauErrorRecoveryType) LUAU_FASTFLAGVARIABLE(LuauInstantiateInSubtyping, false) LUAU_FASTFLAGVARIABLE(LuauUninhabitedSubAnything2, false) LUAU_FASTFLAGVARIABLE(LuauVariadicAnyCanBeGeneric, false) LUAU_FASTFLAGVARIABLE(LuauMaintainScopesInUnifier, false) LUAU_FASTFLAGVARIABLE(LuauTransitiveSubtyping, false) LUAU_FASTFLAGVARIABLE(LuauOccursIsntAlwaysFailure, false) LUAU_FASTFLAG(LuauClassTypeVarsInSubstitution) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) LUAU_FASTFLAG(LuauNormalizeBlockedTypes) LUAU_FASTFLAG(LuauNegatedClassTypes) LUAU_FASTFLAG(LuauNegatedTableTypes) namespace Luau { struct PromoteTypeLevels final : TypeOnceVisitor { TxnLog& log; const TypeArena* typeArena = nullptr; TypeLevel minLevel; Scope* outerScope = nullptr; bool useScopes; PromoteTypeLevels(TxnLog& log, const TypeArena* typeArena, TypeLevel minLevel, Scope* outerScope, bool useScopes) : log(log) , typeArena(typeArena) , minLevel(minLevel) , outerScope(outerScope) , useScopes(useScopes) { } template void promote(TID ty, T* t) { if (FFlag::DebugLuauDeferredConstraintResolution && !t) return; LUAU_ASSERT(t); if (useScopes) { if (subsumesStrict(outerScope, t->scope)) log.changeScope(ty, NotNull{outerScope}); } else { if (minLevel.subsumesStrict(t->level)) { log.changeLevel(ty, minLevel); } } } bool visit(TypeId ty) override { // Type levels of types from other modules are already global, so we don't need to promote anything inside if (ty->owningArena != typeArena) return false; return true; } bool visit(TypePackId tp) override { // Type levels of types from other modules are already global, so we don't need to promote anything inside if (tp->owningArena != typeArena) return false; return true; } bool visit(TypeId ty, const FreeType&) override { // Surprise, it's actually a BoundType that hasn't been committed yet. // Calling getMutable on this will trigger an assertion. if (!log.is(ty)) return true; promote(ty, log.getMutable(ty)); return true; } bool visit(TypeId ty, const FunctionType&) override { // Type levels of types from other modules are already global, so we don't need to promote anything inside 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 (!log.is(ty)) return true; promote(ty, log.getMutable(ty)); return true; } bool visit(TypeId ty, const TableType& ttv) override { // Type levels of types from other modules are already global, so we don't need to promote anything inside if (ty->owningArena != typeArena) return false; 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 (!log.is(ty)) return true; promote(ty, log.getMutable(ty)); return true; } bool visit(TypePackId tp, const FreeTypePack&) override { // Surprise, it's actually a BoundTypePack that hasn't been committed yet. // Calling getMutable on this will trigger an assertion. if (!log.is(tp)) return true; promote(tp, log.getMutable(tp)); return true; } }; static void promoteTypeLevels(TxnLog& log, const TypeArena* typeArena, TypeLevel minLevel, Scope* outerScope, bool useScopes, TypeId ty) { // Type levels of types from other modules are already global, so we don't need to promote anything inside if (ty->owningArena != typeArena) return; PromoteTypeLevels ptl{log, typeArena, minLevel, outerScope, useScopes}; ptl.traverse(ty); } void promoteTypeLevels(TxnLog& log, const TypeArena* typeArena, TypeLevel minLevel, Scope* outerScope, bool useScopes, TypePackId tp) { // Type levels of types from other modules are already global, so we don't need to promote anything inside if (tp->owningArena != typeArena) return; PromoteTypeLevels ptl{log, typeArena, minLevel, outerScope, useScopes}; ptl.traverse(tp); } struct SkipCacheForType final : TypeOnceVisitor { SkipCacheForType(const DenseHashMap& skipCacheForType, const TypeArena* typeArena) : skipCacheForType(skipCacheForType) , typeArena(typeArena) { } bool visit(TypeId, const FreeType&) override { result = true; return false; } bool visit(TypeId, const BoundType&) override { result = true; return false; } bool visit(TypeId, const GenericType&) override { result = true; return false; } bool visit(TypeId, const BlockedType&) override { result = true; return false; } bool visit(TypeId, const PendingExpansionType&) override { result = true; return false; } bool visit(TypeId ty, const TableType&) override { // Types from other modules don't contain mutable elements and are ok to cache if (ty->owningArena != typeArena) return false; TableType& ttv = *getMutable(ty); if (ttv.boundTo) { result = true; return false; } if (ttv.state != TableState::Sealed) { result = true; return false; } return true; } bool visit(TypeId ty) override { // Types from other modules don't contain mutable elements and are ok to cache if (ty->owningArena != typeArena) return false; const bool* prev = skipCacheForType.find(ty); if (prev && *prev) { result = true; return false; } return true; } bool visit(TypePackId tp) override { // Types from other modules don't contain mutable elements and are ok to cache if (tp->owningArena != typeArena) return false; return true; } bool visit(TypePackId tp, const FreeTypePack&) override { result = true; return false; } bool visit(TypePackId tp, const BoundTypePack&) override { result = true; return false; } bool visit(TypePackId tp, const GenericTypePack&) override { result = true; return false; } bool visit(TypePackId tp, const BlockedTypePack&) override { result = true; return false; } const DenseHashMap& skipCacheForType; const TypeArena* typeArena = nullptr; bool result = false; }; bool Widen::isDirty(TypeId ty) { return log->is(ty); } bool Widen::isDirty(TypePackId) { return false; } TypeId Widen::clean(TypeId ty) { LUAU_ASSERT(isDirty(ty)); auto stv = log->getMutable(ty); LUAU_ASSERT(stv); if (get(stv)) return builtinTypes->stringType; else { // If this assert trips, it's likely we now have number singletons. LUAU_ASSERT(get(stv)); return builtinTypes->booleanType; } } TypePackId Widen::clean(TypePackId) { throw InternalCompilerError("Widen attempted to clean a dirty type pack?"); } bool Widen::ignoreChildren(TypeId ty) { if (FFlag::LuauClassTypeVarsInSubstitution && get(ty)) return true; return !log->is(ty); } TypeId Widen::operator()(TypeId ty) { return substitute(ty).value_or(ty); } TypePackId Widen::operator()(TypePackId tp) { return substitute(tp).value_or(tp); } std::optional hasUnificationTooComplex(const ErrorVec& errors) { auto isUnificationTooComplex = [](const TypeError& te) { return nullptr != get(te); }; auto it = std::find_if(errors.begin(), errors.end(), isUnificationTooComplex); if (it == errors.end()) return std::nullopt; else return *it; } // Used for tagged union matching heuristic, returns first singleton type field static std::optional> getTableMatchTag(TypeId type) { if (auto ttv = getTableType(type)) { for (auto&& [name, prop] : ttv->props) { if (auto sing = get(follow(prop.type()))) return {{name, sing}}; } } return std::nullopt; } // TODO: Inline and clip with FFlag::DebugLuauDeferredConstraintResolution template static bool subsumes(bool useScopes, TY_A* left, TY_B* right) { if (useScopes) return subsumes(left->scope, right->scope); else 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) , builtinTypes(normalizer->builtinTypes) , normalizer(normalizer) , mode(mode) , scope(scope) , log(parentLog) , location(location) , variance(variance) , sharedState(*normalizer->sharedState) { LUAU_ASSERT(sharedState.iceHandler); } void Unifier::tryUnify(TypeId subTy, TypeId superTy, bool isFunctionCall, bool isIntersection) { sharedState.counters.iterationCount = 0; tryUnify_(subTy, superTy, isFunctionCall, isIntersection); } static bool isBlocked(const TxnLog& log, TypeId ty) { ty = log.follow(ty); return get(ty) || get(ty); } void Unifier::tryUnify_(TypeId subTy, TypeId superTy, bool isFunctionCall, bool isIntersection) { RecursionLimiter _ra(&sharedState.counters.recursionCount, sharedState.counters.recursionLimit); ++sharedState.counters.iterationCount; if (sharedState.counters.iterationLimit > 0 && sharedState.counters.iterationLimit < sharedState.counters.iterationCount) { reportError(location, UnificationTooComplex{}); return; } superTy = log.follow(superTy); subTy = log.follow(subTy); if (superTy == subTy) return; auto superFree = log.getMutable(superTy); auto subFree = log.getMutable(subTy); if (superFree && subFree && subsumes(useScopes, superFree, subFree)) { if (!occursCheck(subTy, superTy, /* reversed = */ false)) log.replace(subTy, BoundType(superTy)); return; } else if (superFree && subFree) { if (!occursCheck(superTy, subTy, /* reversed = */ true)) { if (subsumes(useScopes, superFree, subFree)) { log.changeLevel(subTy, superFree->level); } log.replace(superTy, BoundType(subTy)); } return; } else if (superFree) { // Unification can't change the level of a generic. auto subGeneric = log.getMutable(subTy); if (subGeneric && !subsumes(useScopes, subGeneric, superFree)) { // TODO: a more informative error message? CLI-39912 reportError(location, GenericError{"Generic subtype escaping scope"}); return; } if (!occursCheck(superTy, subTy, /* reversed = */ true)) { promoteTypeLevels(log, types, superFree->level, superFree->scope, useScopes, subTy); Widen widen{types, builtinTypes}; log.replace(superTy, BoundType(widen(subTy))); } return; } else if (subFree) { // Normally, if the subtype is free, it should not be bound to any, unknown, or error types. // But for bug compatibility, we'll only apply this rule to unknown. Doing this will silence cascading type errors. if (log.get(superTy)) return; // Unification can't change the level of a generic. auto superGeneric = log.getMutable(superTy); if (superGeneric && !subsumes(useScopes, superGeneric, subFree)) { // TODO: a more informative error message? CLI-39912 reportError(location, GenericError{"Generic supertype escaping scope"}); return; } if (!occursCheck(subTy, superTy, /* reversed = */ false)) { promoteTypeLevels(log, types, subFree->level, subFree->scope, useScopes, superTy); log.replace(subTy, BoundType(superTy)); } return; } if (log.get(superTy)) return tryUnifyWithAny(subTy, builtinTypes->anyType); if (!FFlag::LuauTransitiveSubtyping && log.get(superTy)) return tryUnifyWithAny(subTy, builtinTypes->errorType); if (!FFlag::LuauTransitiveSubtyping && log.get(superTy)) return tryUnifyWithAny(subTy, builtinTypes->unknownType); if (log.get(subTy)) { if (FFlag::LuauTransitiveSubtyping && normalize) { // TODO: there are probably cheaper ways to check if any <: T. const NormalizedType* superNorm = normalizer->normalize(superTy); if (!log.get(superNorm->tops)) failure = true; } else failure = true; return tryUnifyWithAny(superTy, builtinTypes->anyType); } if (!FFlag::LuauTransitiveSubtyping && log.get(subTy)) return tryUnifyWithAny(superTy, builtinTypes->errorType); if (log.get(subTy)) return tryUnifyWithAny(superTy, builtinTypes->neverType); auto& cache = sharedState.cachedUnify; // What if the types are immutable and we proved their relation before bool cacheEnabled = !isFunctionCall && !isIntersection && variance == Invariant; if (cacheEnabled) { if (cache.contains({subTy, superTy})) return; if (auto error = sharedState.cachedUnifyError.find({subTy, superTy})) { reportError(location, *error); return; } } // If we have seen this pair of types before, we are currently recursing into cyclic types. // Here, we assume that the types unify. If they do not, we will find out as we roll back // the stack. if (log.haveSeen(superTy, subTy)) return; log.pushSeen(superTy, subTy); size_t errorCount = errors.size(); if (isBlocked(log, subTy) && isBlocked(log, superTy)) { blockedTypes.push_back(subTy); blockedTypes.push_back(superTy); } else if (isBlocked(log, subTy)) blockedTypes.push_back(subTy); else if (isBlocked(log, superTy)) blockedTypes.push_back(superTy); else if (const UnionType* subUnion = log.getMutable(subTy)) { tryUnifyUnionWithType(subTy, subUnion, superTy); } else if (const IntersectionType* uv = log.getMutable(superTy)) { tryUnifyTypeWithIntersection(subTy, superTy, uv); } else if (const UnionType* uv = log.getMutable(superTy)) { tryUnifyTypeWithUnion(subTy, superTy, uv, cacheEnabled, isFunctionCall); } else if (const IntersectionType* uv = log.getMutable(subTy)) { tryUnifyIntersectionWithType(subTy, uv, superTy, cacheEnabled, isFunctionCall); } else if (FFlag::LuauTransitiveSubtyping && log.get(subTy)) { tryUnifyWithAny(superTy, builtinTypes->unknownType); failure = true; } else if (FFlag::LuauTransitiveSubtyping && log.get(subTy) && log.get(superTy)) { // error <: error } else if (FFlag::LuauTransitiveSubtyping && log.get(superTy)) { tryUnifyWithAny(subTy, builtinTypes->errorType); failure = true; } else if (FFlag::LuauTransitiveSubtyping && log.get(subTy)) { tryUnifyWithAny(superTy, builtinTypes->errorType); failure = true; } else if (FFlag::LuauTransitiveSubtyping && log.get(superTy)) { // At this point, all the supertypes of `error` have been handled, // and if `error unknownType); } else if (FFlag::LuauTransitiveSubtyping && log.get(superTy)) { tryUnifyWithAny(subTy, builtinTypes->unknownType); } else if (log.getMutable(superTy) && log.getMutable(subTy)) tryUnifyPrimitives(subTy, superTy); else if ((log.getMutable(superTy) || log.getMutable(superTy)) && log.getMutable(subTy)) tryUnifySingletons(subTy, superTy); else if (auto ptv = get(superTy); ptv && ptv->type == PrimitiveType::Function && get(subTy)) { // Ok. Do nothing. forall functions F, F <: function } else if (FFlag::LuauNegatedTableTypes && isPrim(superTy, PrimitiveType::Table) && (get(subTy) || get(subTy))) { // Ok, do nothing: forall tables T, T <: table } else if (log.getMutable(superTy) && log.getMutable(subTy)) tryUnifyFunctions(subTy, superTy, isFunctionCall); else if (auto table = log.get(superTy); table && table->type == PrimitiveType::Table) tryUnify(subTy, builtinTypes->emptyTableType, isFunctionCall, isIntersection); else if (auto table = log.get(subTy); table && table->type == PrimitiveType::Table) tryUnify(builtinTypes->emptyTableType, superTy, isFunctionCall, isIntersection); else if (log.getMutable(superTy) && log.getMutable(subTy)) { tryUnifyTables(subTy, superTy, isIntersection); } else if (log.get(superTy) && (log.get(subTy) || log.get(subTy))) { tryUnifyScalarShape(subTy, superTy, /*reversed*/ false); } else if (log.get(subTy) && (log.get(superTy) || log.get(superTy))) { tryUnifyScalarShape(subTy, superTy, /*reversed*/ true); } // tryUnifyWithMetatable assumes its first argument is a MetatableType. The check is otherwise symmetrical. else if (log.getMutable(superTy)) tryUnifyWithMetatable(subTy, superTy, /*reversed*/ false); else if (log.getMutable(subTy)) tryUnifyWithMetatable(superTy, subTy, /*reversed*/ true); else if (log.getMutable(superTy)) tryUnifyWithClass(subTy, superTy, /*reversed*/ false); // Unification of nonclasses with classes is almost, but not quite symmetrical. // The order in which we perform this test is significant in the case that both types are classes. else if (log.getMutable(subTy)) tryUnifyWithClass(subTy, superTy, /*reversed*/ true); else if (log.get(superTy) || log.get(subTy)) tryUnifyNegations(subTy, superTy); else if (FFlag::LuauUninhabitedSubAnything2 && checkInhabited && !normalizer->isInhabited(subTy)) { } else reportError(location, TypeMismatch{superTy, subTy, mismatchContext()}); if (cacheEnabled) cacheResult(subTy, superTy, errorCount); log.popSeen(superTy, subTy); } void Unifier::tryUnifyUnionWithType(TypeId subTy, const UnionType* subUnion, TypeId superTy) { // A | B <: T if and only if A <: T and B <: T bool failed = false; bool errorsSuppressed = true; 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 (FFlag::LuauTransitiveSubtyping ? innerState.failure : !innerState.errors.empty()) { // If errors were suppressed, we store the log up, so we can commit it if no other option succeeds. if (FFlag::LuauTransitiveSubtyping && innerState.errors.empty()) logs.push_back(std::move(innerState.log)); // 'nil' option is skipped from extended report because we present the type in a special way - 'T?' else if (!firstFailedOption && !isNil(type)) firstFailedOption = {innerState.errors.front()}; failed = true; errorsSuppressed &= innerState.errors.empty(); } } if (FFlag::DebugLuauDeferredConstraintResolution) log.concatAsUnion(combineLogsIntoUnion(std::move(logs)), NotNull{types}); else { // 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, mismatchContext()}); else if (!FFlag::LuauTransitiveSubtyping || !errorsSuppressed) reportError(location, TypeMismatch{superTy, subTy, mismatchContext()}); failure = true; } } struct DEPRECATED_BlockedTypeFinder : TypeOnceVisitor { std::unordered_set blockedTypes; bool visit(TypeId ty, const BlockedType&) override { blockedTypes.insert(ty); return true; } }; bool Unifier::DEPRECATED_blockOnBlockedTypes(TypeId subTy, TypeId superTy) { LUAU_ASSERT(!FFlag::LuauNormalizeBlockedTypes); DEPRECATED_BlockedTypeFinder blockedTypeFinder; blockedTypeFinder.traverse(subTy); blockedTypeFinder.traverse(superTy); if (!blockedTypeFinder.blockedTypes.empty()) { blockedTypes.insert(end(blockedTypes), begin(blockedTypeFinder.blockedTypes), end(blockedTypeFinder.blockedTypes)); return true; } return false; } void Unifier::tryUnifyTypeWithUnion(TypeId subTy, TypeId superTy, const UnionType* uv, bool cacheEnabled, bool isFunctionCall) { // T <: A | B if T <: A or T <: B bool found = false; bool errorsSuppressed = false; std::optional unificationTooComplex; size_t failedOptionCount = 0; std::optional failedOption; bool foundHeuristic = false; size_t startIndex = 0; if (const std::string* subName = getName(subTy)) { for (size_t i = 0; i < uv->options.size(); ++i) { const std::string* optionName = getName(uv->options[i]); if (optionName && *optionName == *subName) { foundHeuristic = true; startIndex = i; break; } } } if (auto subMatchTag = getTableMatchTag(subTy)) { for (size_t i = 0; i < uv->options.size(); ++i) { auto optionMatchTag = getTableMatchTag(uv->options[i]); if (optionMatchTag && optionMatchTag->first == subMatchTag->first && *optionMatchTag->second == *subMatchTag->second) { foundHeuristic = true; startIndex = i; break; } } } if (FFlag::LuauTransitiveSubtyping && !foundHeuristic) { for (size_t i = 0; i < uv->options.size(); ++i) { TypeId type = uv->options[i]; if (subTy == type) { foundHeuristic = true; startIndex = i; break; } } } if (!foundHeuristic && cacheEnabled) { auto& cache = sharedState.cachedUnify; for (size_t i = 0; i < uv->options.size(); ++i) { TypeId type = uv->options[i]; if (cache.contains({subTy, type})) { startIndex = i; break; } } } std::vector logs; for (size_t i = 0; i < uv->options.size(); ++i) { TypeId type = uv->options[(i + startIndex) % uv->options.size()]; Unifier innerState = makeChildUnifier(); innerState.normalize = false; innerState.tryUnify_(subTy, type, isFunctionCall); if (FFlag::LuauTransitiveSubtyping ? !innerState.failure : innerState.errors.empty()) { found = true; if (FFlag::DebugLuauDeferredConstraintResolution) logs.push_back(std::move(innerState.log)); else { log.concat(std::move(innerState.log)); break; } } else if (FFlag::LuauTransitiveSubtyping && innerState.errors.empty()) { errorsSuppressed = true; } else if (auto e = hasUnificationTooComplex(innerState.errors)) { unificationTooComplex = e; } else if (!isNil(type)) { failedOptionCount++; if (!failedOption) failedOption = {innerState.errors.front()}; } } if (FFlag::DebugLuauDeferredConstraintResolution) log.concatAsUnion(combineLogsIntoUnion(std::move(logs)), NotNull{types}); if (unificationTooComplex) { reportError(*unificationTooComplex); } else if (FFlag::LuauTransitiveSubtyping && !found && normalize) { // It is possible that T <: A | B even though T normalize(subTy); const NormalizedType* superNorm = normalizer->normalize(superTy); Unifier innerState = makeChildUnifier(); if (!subNorm || !superNorm) return reportError(location, UnificationTooComplex{}); else if ((failedOptionCount == 1 || foundHeuristic) && failedOption) innerState.tryUnifyNormalizedTypes( subTy, superTy, *subNorm, *superNorm, "None of the union options are compatible. For example:", *failedOption); else innerState.tryUnifyNormalizedTypes(subTy, superTy, *subNorm, *superNorm, "none of the union options are compatible"); if (!innerState.failure) log.concat(std::move(innerState.log)); else if (errorsSuppressed || innerState.errors.empty()) failure = true; else reportError(std::move(innerState.errors.front())); } else if (!found && normalize) { // We cannot normalize a type that contains blocked types. We have to // stop for now if we find any. if (!FFlag::LuauNormalizeBlockedTypes && DEPRECATED_blockOnBlockedTypes(subTy, superTy)) return; // It is possible that T <: A | B even though T normalize(subTy); const NormalizedType* superNorm = normalizer->normalize(superTy); if (!subNorm || !superNorm) reportError(location, UnificationTooComplex{}); else if ((failedOptionCount == 1 || foundHeuristic) && failedOption) tryUnifyNormalizedTypes(subTy, superTy, *subNorm, *superNorm, "None of the union options are compatible. For example:", *failedOption); else tryUnifyNormalizedTypes(subTy, superTy, *subNorm, *superNorm, "none of the union options are compatible"); } else if (!found) { if (FFlag::LuauTransitiveSubtyping && errorsSuppressed) failure = true; else if ((failedOptionCount == 1 || foundHeuristic) && 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", mismatchContext()}); } } void Unifier::tryUnifyTypeWithIntersection(TypeId subTy, TypeId superTy, const IntersectionType* uv) { 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) { Unifier innerState = makeChildUnifier(); innerState.tryUnify_(subTy, type, /*isFunctionCall*/ false, /*isIntersection*/ true); if (auto e = hasUnificationTooComplex(innerState.errors)) unificationTooComplex = e; else if (!innerState.errors.empty()) { if (!firstFailedOption) firstFailedOption = {innerState.errors.front()}; } if (FFlag::DebugLuauDeferredConstraintResolution) logs.push_back(std::move(innerState.log)); else log.concat(std::move(innerState.log)); failure |= innerState.failure; } 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, mismatchContext()}); } struct NegationTypeFinder : TypeOnceVisitor { bool found = false; bool visit(TypeId ty) override { return !found; } bool visit(TypeId ty, const NegationType&) override { found = true; return !found; } }; void Unifier::tryUnifyIntersectionWithType(TypeId subTy, const IntersectionType* uv, TypeId superTy, bool cacheEnabled, bool isFunctionCall) { // A & B <: T if A <: T or B <: T bool found = false; bool errorsSuppressed = false; std::optional unificationTooComplex; size_t startIndex = 0; if (cacheEnabled) { auto& cache = sharedState.cachedUnify; for (size_t i = 0; i < uv->parts.size(); ++i) { TypeId type = uv->parts[i]; if (cache.contains({type, superTy})) { startIndex = i; break; } } } if (FFlag::DebugLuauDeferredConstraintResolution && normalize) { // We cannot normalize a type that contains blocked types. We have to // stop for now if we find any. if (!FFlag::LuauNormalizeBlockedTypes && DEPRECATED_blockOnBlockedTypes(subTy, superTy)) return; // Sometimes a negation type is inside one of the types, e.g. { p: number } & { p: ~number }. NegationTypeFinder finder; finder.traverse(subTy); if (finder.found) { // It is possible that A & B <: T even though A normalize(subTy); const NormalizedType* superNorm = normalizer->normalize(superTy); if (subNorm && superNorm) tryUnifyNormalizedTypes(subTy, superTy, *subNorm, *superNorm, "none of the intersection parts are compatible"); else reportError(location, UnificationTooComplex{}); return; } } std::vector logs; for (size_t i = 0; i < uv->parts.size(); ++i) { TypeId type = uv->parts[(i + startIndex) % uv->parts.size()]; Unifier innerState = makeChildUnifier(); innerState.normalize = false; innerState.tryUnify_(type, superTy, isFunctionCall); // TODO: This sets errorSuppressed to true if any of the parts is error-suppressing, // in paricular any & T is error-suppressing. Really, errorSuppressed should be true if // all of the parts are error-suppressing, but that fails to typecheck lua-apps. if (innerState.errors.empty()) { found = true; errorsSuppressed = innerState.failure; if (FFlag::DebugLuauDeferredConstraintResolution || (FFlag::LuauTransitiveSubtyping && innerState.failure)) logs.push_back(std::move(innerState.log)); else { errorsSuppressed = false; log.concat(std::move(innerState.log)); break; } } else if (auto e = hasUnificationTooComplex(innerState.errors)) { unificationTooComplex = e; } } if (FFlag::DebugLuauDeferredConstraintResolution) log.concat(combineLogsIntoIntersection(std::move(logs))); else if (FFlag::LuauTransitiveSubtyping && errorsSuppressed) log.concat(std::move(logs.front())); if (unificationTooComplex) reportError(*unificationTooComplex); else if (!found && normalize) { // We cannot normalize a type that contains blocked types. We have to // stop for now if we find any. if (!FFlag::LuauNormalizeBlockedTypes && DEPRECATED_blockOnBlockedTypes(subTy, superTy)) return; // It is possible that A & B <: T even though A normalize(subTy); const NormalizedType* superNorm = normalizer->normalize(superTy); if (subNorm && superNorm) tryUnifyNormalizedTypes(subTy, superTy, *subNorm, *superNorm, "none of the intersection parts are compatible"); else reportError(location, UnificationTooComplex{}); } else if (!found) { reportError(location, TypeMismatch{superTy, subTy, "none of the intersection parts are compatible", mismatchContext()}); } else if (errorsSuppressed) failure = true; } void Unifier::tryUnifyNormalizedTypes( TypeId subTy, TypeId superTy, const NormalizedType& subNorm, const NormalizedType& superNorm, std::string reason, std::optional error) { if (!FFlag::LuauTransitiveSubtyping && get(superNorm.tops)) return; else if (get(superNorm.tops)) return; else if (get(subNorm.tops)) { failure = true; return; } else if (!FFlag::LuauTransitiveSubtyping && get(subNorm.tops)) return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()}); if (get(subNorm.errors)) if (!get(superNorm.errors)) { failure = true; if (!FFlag::LuauTransitiveSubtyping) reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()}); return; } if (FFlag::LuauTransitiveSubtyping && get(superNorm.tops)) return; if (FFlag::LuauTransitiveSubtyping && get(subNorm.tops)) 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, mismatchContext()}); } else if (const SingletonType* stv = get(subNorm.booleans)) { if (!get(superNorm.booleans) && stv != get(superNorm.booleans)) 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, mismatchContext()}); if (get(subNorm.numbers)) if (!get(superNorm.numbers)) return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()}); if (!isSubtype(subNorm.strings, superNorm.strings)) 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, mismatchContext()}); if (FFlag::LuauNegatedClassTypes) { for (const auto& [subClass, _] : subNorm.classes.classes) { bool found = false; const ClassType* subCtv = get(subClass); LUAU_ASSERT(subCtv); for (const auto& [superClass, superNegations] : superNorm.classes.classes) { const ClassType* superCtv = get(superClass); LUAU_ASSERT(superCtv); if (isSubclass(subCtv, superCtv)) { found = true; for (TypeId negation : superNegations) { const ClassType* negationCtv = get(negation); LUAU_ASSERT(negationCtv); if (isSubclass(subCtv, negationCtv)) { found = false; break; } } if (found) break; } } if (FFlag::DebugLuauDeferredConstraintResolution) { for (TypeId superTable : superNorm.tables) { Unifier innerState = makeChildUnifier(); innerState.tryUnify(subClass, superTable); if (innerState.errors.empty()) { found = true; log.concat(std::move(innerState.log)); break; } else if (auto e = hasUnificationTooComplex(innerState.errors)) return reportError(*e); } } if (!found) { return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()}); } } } else { for (TypeId subClass : subNorm.DEPRECATED_classes) { bool found = false; const ClassType* subCtv = get(subClass); for (TypeId superClass : superNorm.DEPRECATED_classes) { const ClassType* superCtv = get(superClass); if (isSubclass(subCtv, superCtv)) { found = true; break; } } if (!found) return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()}); } } for (TypeId subTable : subNorm.tables) { bool found = false; for (TypeId superTable : superNorm.tables) { if (FFlag::LuauNegatedTableTypes && isPrim(superTable, PrimitiveType::Table)) { found = true; break; } Unifier innerState = makeChildUnifier(); innerState.tryUnify(subTable, superTable); if (innerState.errors.empty()) { found = true; log.concat(std::move(innerState.log)); break; } else if (auto e = hasUnificationTooComplex(innerState.errors)) return reportError(*e); } if (!found) 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, mismatchContext()}); for (TypeId superFun : superNorm.functions.parts) { Unifier innerState = makeChildUnifier(); const FunctionType* superFtv = get(superFun); if (!superFtv) 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()) log.concat(std::move(innerState.log)); else if (auto e = hasUnificationTooComplex(innerState.errors)) return reportError(*e); else return reportError(location, TypeMismatch{superTy, subTy, reason, error, mismatchContext()}); } } for (auto& [tyvar, subIntersect] : subNorm.tyvars) { auto found = superNorm.tyvars.find(tyvar); if (found == superNorm.tyvars.end()) tryUnifyNormalizedTypes(subTy, superTy, *subIntersect, superNorm, reason, error); else tryUnifyNormalizedTypes(subTy, superTy, *subIntersect, *found->second, reason, error); if (!errors.empty()) return; } } TypePackId Unifier::tryApplyOverloadedFunction(TypeId function, const NormalizedFunctionType& overloads, TypePackId args) { if (overloads.isNever()) { reportError(location, CannotCallNonFunction{function}); return builtinTypes->errorRecoveryTypePack(); } std::optional result; const FunctionType* firstFun = nullptr; for (TypeId overload : overloads.parts) { if (const FunctionType* ftv = get(overload)) { // TODO: instantiate generics? if (ftv->generics.empty() && ftv->genericPacks.empty()) { if (!firstFun) firstFun = ftv; Unifier innerState = makeChildUnifier(); innerState.tryUnify_(args, ftv->argTypes); if (innerState.errors.empty()) { log.concat(std::move(innerState.log)); if (result) { innerState.log.clear(); innerState.tryUnify_(*result, ftv->retTypes); if (innerState.errors.empty()) log.concat(std::move(innerState.log)); // Annoyingly, since we don't support intersection of generic type packs, // the intersection may fail. We rather arbitrarily use the first matching overload // in that case. else if (std::optional intersect = normalizer->intersectionOfTypePacks(*result, ftv->retTypes)) result = intersect; } else result = ftv->retTypes; } else if (auto e = hasUnificationTooComplex(innerState.errors)) { reportError(*e); return builtinTypes->errorRecoveryTypePack(args); } } } } if (result) return *result; else if (firstFun) { // TODO: better error reporting? // The logic for error reporting overload resolution // is currently over in TypeInfer.cpp, should we move it? reportError(location, GenericError{"No matching overload."}); return builtinTypes->errorRecoveryTypePack(firstFun->retTypes); } else { reportError(location, CannotCallNonFunction{function}); return builtinTypes->errorRecoveryTypePack(); } } bool Unifier::canCacheResult(TypeId subTy, TypeId superTy) { bool* superTyInfo = sharedState.skipCacheForType.find(superTy); if (superTyInfo && *superTyInfo) return false; bool* subTyInfo = sharedState.skipCacheForType.find(subTy); if (subTyInfo && *subTyInfo) return false; auto skipCacheFor = [this](TypeId ty) { SkipCacheForType visitor{sharedState.skipCacheForType, types}; visitor.traverse(ty); sharedState.skipCacheForType[ty] = visitor.result; return visitor.result; }; if (!superTyInfo && skipCacheFor(superTy)) return false; if (!subTyInfo && skipCacheFor(subTy)) return false; return true; } void Unifier::cacheResult(TypeId subTy, TypeId superTy, size_t prevErrorCount) { if (errors.size() == prevErrorCount) { if (canCacheResult(subTy, superTy)) sharedState.cachedUnify.insert({subTy, superTy}); } else if (errors.size() == prevErrorCount + 1) { if (canCacheResult(subTy, superTy)) sharedState.cachedUnifyError[{subTy, superTy}] = errors.back().data; } } struct WeirdIter { TypePackId packId; TxnLog& log; TypePack* pack; size_t index; bool growing; TypeLevel level; Scope* scope = nullptr; WeirdIter(TypePackId packId, TxnLog& log) : packId(packId) , log(log) , pack(log.getMutable(packId)) , index(0) , growing(false) { while (pack && pack->head.empty() && pack->tail) { packId = *pack->tail; pack = log.getMutable(packId); } } WeirdIter(const WeirdIter&) = default; TypeId& operator*() { LUAU_ASSERT(good()); return pack->head[index]; } bool good() const { return pack != nullptr && index < pack->head.size(); } bool advance() { if (!pack) return good(); if (index < pack->head.size()) ++index; if (growing || index < pack->head.size()) return good(); if (pack->tail) { packId = log.follow(*pack->tail); pack = log.getMutable(packId); index = 0; } return good(); } bool canGrow() const { return nullptr != log.getMutable(packId); } void grow(TypePackId newTail) { LUAU_ASSERT(canGrow()); LUAU_ASSERT(log.getMutable(newTail)); auto freePack = log.getMutable(packId); level = freePack->level; if (FFlag::LuauMaintainScopesInUnifier && freePack->scope != nullptr) scope = freePack->scope; log.replace(packId, BoundTypePack(newTail)); packId = newTail; pack = log.getMutable(newTail); index = 0; growing = true; } void pushType(TypeId ty) { LUAU_ASSERT(pack); PendingTypePack* pendingPack = log.queue(packId); if (TypePack* pending = getMutable(pendingPack)) { pending->head.push_back(ty); // We've potentially just replaced the TypePack* that we need to look // in. We need to replace pack. pack = pending; } else { LUAU_ASSERT(!"Pending state for this pack was not a TypePack"); } } }; ErrorVec Unifier::canUnify(TypeId subTy, TypeId superTy) { Unifier s = makeChildUnifier(); s.tryUnify_(subTy, superTy); return s.errors; } ErrorVec Unifier::canUnify(TypePackId subTy, TypePackId superTy, bool isFunctionCall) { Unifier s = makeChildUnifier(); s.tryUnify_(subTy, superTy, isFunctionCall); return s.errors; } void Unifier::tryUnify(TypePackId subTp, TypePackId superTp, bool isFunctionCall) { sharedState.counters.iterationCount = 0; tryUnify_(subTp, superTp, isFunctionCall); } /* * This is quite tricky: we are walking two rope-like structures and unifying corresponding elements. * If one is longer than the other, but the short end is free, we grow it to the required length. */ void Unifier::tryUnify_(TypePackId subTp, TypePackId superTp, bool isFunctionCall) { RecursionLimiter _ra(&sharedState.counters.recursionCount, sharedState.counters.recursionLimit); ++sharedState.counters.iterationCount; if (sharedState.counters.iterationLimit > 0 && sharedState.counters.iterationLimit < sharedState.counters.iterationCount) { reportError(location, UnificationTooComplex{}); return; } superTp = log.follow(superTp); subTp = log.follow(subTp); while (auto tp = log.getMutable(subTp)) { if (tp->head.empty() && tp->tail) subTp = log.follow(*tp->tail); else break; } while (auto tp = log.getMutable(superTp)) { if (tp->head.empty() && tp->tail) superTp = log.follow(*tp->tail); else break; } if (superTp == subTp) return; if (log.haveSeen(superTp, subTp)) return; if (log.getMutable(superTp)) { if (!occursCheck(superTp, subTp, /* reversed = */ true)) { Widen widen{types, builtinTypes}; log.replace(superTp, Unifiable::Bound(widen(subTp))); } } else if (log.getMutable(subTp)) { if (!occursCheck(subTp, superTp, /* reversed = */ false)) { log.replace(subTp, Unifiable::Bound(superTp)); } } else if (log.getMutable(superTp)) tryUnifyWithAny(subTp, superTp); else if (log.getMutable(subTp)) tryUnifyWithAny(superTp, subTp); else if (log.getMutable(superTp)) tryUnifyVariadics(subTp, superTp, false); else if (log.getMutable(subTp)) tryUnifyVariadics(superTp, subTp, true); else if (log.getMutable(superTp) && log.getMutable(subTp)) { auto superTpv = log.getMutable(superTp); auto subTpv = log.getMutable(subTp); // If the size of two heads does not match, but both packs have free tail // We set the sentinel variable to say so to avoid growing it forever. auto [superTypes, superTail] = flatten(superTp, log); auto [subTypes, subTail] = flatten(subTp, log); bool noInfiniteGrowth = (superTypes.size() != subTypes.size()) && (superTail && log.getMutable(*superTail)) && (subTail && log.getMutable(*subTail)); auto superIter = WeirdIter(superTp, log); auto subIter = WeirdIter(subTp, log); if (FFlag::LuauMaintainScopesInUnifier) { superIter.scope = scope.get(); subIter.scope = scope.get(); } auto mkFreshType = [this](Scope* scope, TypeLevel level) { return types->freshType(scope, level); }; const TypePackId emptyTp = types->addTypePack(TypePack{{}, std::nullopt}); int loopCount = 0; do { if (FInt::LuauTypeInferTypePackLoopLimit > 0 && loopCount >= FInt::LuauTypeInferTypePackLoopLimit) ice("Detected possibly infinite TypePack growth"); ++loopCount; if (superIter.good() && subIter.growing) { subIter.pushType(mkFreshType(subIter.scope, subIter.level)); } if (subIter.good() && superIter.growing) { superIter.pushType(mkFreshType(superIter.scope, superIter.level)); } if (superIter.good() && subIter.good()) { tryUnify_(*subIter, *superIter); if (!errors.empty() && !firstPackErrorPos) firstPackErrorPos = loopCount; superIter.advance(); subIter.advance(); continue; } // If both are at the end, we're done if (!superIter.good() && !subIter.good()) { 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 && rFreeTail) { tryUnify_(*subTpv->tail, *superTpv->tail); } else if (lFreeTail) { tryUnify_(emptyTp, *superTpv->tail); } else if (rFreeTail) { tryUnify_(emptyTp, *subTpv->tail); } else if (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; } // If both tails are free, bind one to the other and call it a day if (superIter.canGrow() && subIter.canGrow()) return tryUnify_(*subIter.pack->tail, *superIter.pack->tail); // If just one side is free on its tail, grow it to fit the other side. // FIXME: The tail-most tail of the growing pack should be the same as the tail-most tail of the non-growing pack. if (superIter.canGrow()) superIter.grow(types->addTypePack(TypePackVar(TypePack{}))); else if (subIter.canGrow()) subIter.grow(types->addTypePack(TypePackVar(TypePack{}))); else { // A union type including nil marks an optional argument if (superIter.good() && isOptional(*superIter)) { superIter.advance(); continue; } else if (subIter.good() && isOptional(*subIter)) { subIter.advance(); continue; } if (log.getMutable(superIter.packId)) { tryUnifyVariadics(subIter.packId, superIter.packId, false, int(subIter.index)); return; } if (log.getMutable(subIter.packId)) { tryUnifyVariadics(superIter.packId, subIter.packId, true, int(superIter.index)); return; } if (!isFunctionCall && subIter.good()) { // Sometimes it is ok to pass too many arguments return; } // This is a bit weird because we don't actually know expected vs actual. We just know // subtype vs supertype. If we are checking the values returned by a function, we swap // these to produce the expected error message. size_t expectedSize = size(superTp); size_t actualSize = size(subTp); if (ctx == CountMismatch::FunctionResult || ctx == CountMismatch::ExprListResult) std::swap(expectedSize, actualSize); reportError(location, CountMismatch{expectedSize, std::nullopt, actualSize, ctx}); while (superIter.good()) { tryUnify_(*superIter, builtinTypes->errorRecoveryType()); superIter.advance(); } while (subIter.good()) { tryUnify_(*subIter, builtinTypes->errorRecoveryType()); subIter.advance(); } return; } } while (!noInfiniteGrowth); } else { reportError(location, TypePackMismatch{subTp, superTp}); } } void Unifier::tryUnifyPrimitives(TypeId subTy, TypeId superTy) { const PrimitiveType* superPrim = get(superTy); const PrimitiveType* subPrim = get(subTy); if (!superPrim || !subPrim) ice("passed non primitive types to unifyPrimitives"); if (superPrim->type != subPrim->type) reportError(location, TypeMismatch{superTy, subTy, mismatchContext()}); } void Unifier::tryUnifySingletons(TypeId subTy, TypeId superTy) { const PrimitiveType* superPrim = get(superTy); const SingletonType* superSingleton = get(superTy); const SingletonType* subSingleton = get(subTy); if ((!superPrim && !superSingleton) || !subSingleton) ice("passed non singleton/primitive types to unifySingletons"); if (superSingleton && *superSingleton == *subSingleton) return; if (superPrim && superPrim->type == PrimitiveType::Boolean && get(subSingleton) && variance == Covariant) return; if (superPrim && superPrim->type == PrimitiveType::String && get(subSingleton) && variance == Covariant) return; reportError(location, TypeMismatch{superTy, subTy, mismatchContext()}); } void Unifier::tryUnifyFunctions(TypeId subTy, TypeId superTy, bool isFunctionCall) { FunctionType* superFunction = log.getMutable(superTy); FunctionType* subFunction = log.getMutable(subTy); if (!superFunction || !subFunction) ice("passed non-function types to unifyFunction"); size_t numGenerics = superFunction->generics.size(); size_t numGenericPacks = superFunction->genericPacks.size(); bool shouldInstantiate = (numGenerics == 0 && subFunction->generics.size() > 0) || (numGenericPacks == 0 && subFunction->genericPacks.size() > 0); // TODO: This is unsound when the context is invariant, but the annotation burden without allowing it and without // read-only properties is too high for lua-apps. Read-only properties _should_ resolve their issue by allowing // generic methods in tables to be marked read-only. if (FFlag::LuauInstantiateInSubtyping && shouldInstantiate) { Instantiation instantiation{&log, types, scope->level, scope}; std::optional instantiated = instantiation.substitute(subTy); if (instantiated.has_value()) { subFunction = log.getMutable(*instantiated); if (!subFunction) ice("instantiation made a function type into a non-function type in unifyFunction"); numGenerics = std::min(superFunction->generics.size(), subFunction->generics.size()); numGenericPacks = std::min(superFunction->genericPacks.size(), subFunction->genericPacks.size()); } else { reportError(location, UnificationTooComplex{}); } } else if (numGenerics != subFunction->generics.size()) { numGenerics = std::min(superFunction->generics.size(), subFunction->generics.size()); 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", mismatchContext()}); } for (size_t i = 0; i < numGenerics; i++) { log.pushSeen(superFunction->generics[i], subFunction->generics[i]); } for (size_t i = 0; i < numGenericPacks; i++) { log.pushSeen(superFunction->genericPacks[i], subFunction->genericPacks[i]); } CountMismatch::Context context = ctx; if (!isFunctionCall) { Unifier innerState = makeChildUnifier(); innerState.ctx = CountMismatch::Arg; innerState.tryUnify_(superFunction->argTypes, subFunction->argTypes, isFunctionCall); bool reported = !innerState.errors.empty(); if (auto e = hasUnificationTooComplex(innerState.errors)) 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(), mismatchContext()}); else if (!innerState.errors.empty()) reportError(location, TypeMismatch{superTy, subTy, "", innerState.errors.front(), mismatchContext()}); innerState.ctx = CountMismatch::FunctionResult; innerState.tryUnify_(subFunction->retTypes, superFunction->retTypes); if (!reported) { 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(), 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(), mismatchContext()}); else if (!innerState.errors.empty()) reportError(location, TypeMismatch{superTy, subTy, "", innerState.errors.front(), mismatchContext()}); } log.concat(std::move(innerState.log)); } else { ctx = CountMismatch::Arg; tryUnify_(superFunction->argTypes, subFunction->argTypes, isFunctionCall); ctx = CountMismatch::FunctionResult; tryUnify_(subFunction->retTypes, superFunction->retTypes); } // Updating the log may have invalidated the function pointers superFunction = log.getMutable(superTy); subFunction = log.getMutable(subTy); ctx = context; for (int i = int(numGenericPacks) - 1; 0 <= i; i--) { log.popSeen(superFunction->genericPacks[i], subFunction->genericPacks[i]); } for (int i = int(numGenerics) - 1; 0 <= i; i--) { log.popSeen(superFunction->generics[i], subFunction->generics[i]); } } namespace { struct Resetter { explicit Resetter(Variance* variance) : oldValue(*variance) , variance(variance) { } Variance oldValue; Variance* variance; ~Resetter() { *variance = oldValue; } }; } // namespace void Unifier::tryUnifyTables(TypeId subTy, TypeId superTy, bool isIntersection) { if (isPrim(log.follow(subTy), PrimitiveType::Table)) subTy = builtinTypes->emptyTableType; if (isPrim(log.follow(superTy), PrimitiveType::Table)) superTy = builtinTypes->emptyTableType; TypeId activeSubTy = subTy; TableType* superTable = log.getMutable(superTy); TableType* subTable = log.getMutable(subTy); if (!superTable || !subTable) ice("passed non-table types to unifyTables"); std::vector missingProperties; std::vector extraProperties; if (FFlag::LuauInstantiateInSubtyping) { if (variance == Covariant && subTable->state == TableState::Generic && superTable->state != TableState::Generic) { Instantiation instantiation{&log, types, subTable->level, scope}; std::optional instantiated = instantiation.substitute(subTy); if (instantiated.has_value()) { activeSubTy = *instantiated; subTable = log.getMutable(activeSubTy); if (!subTable) ice("instantiation made a table type into a non-table type in tryUnifyTables"); } else { reportError(location, UnificationTooComplex{}); } } } // Optimization: First test that the property sets are compatible without doing any recursive unification if (!subTable->indexer && subTable->state != TableState::Free) { for (const auto& [propName, superProp] : superTable->props) { auto subIter = subTable->props.find(propName); if (subIter == subTable->props.end() && subTable->state == TableState::Unsealed && !isOptional(superProp.type())) missingProperties.push_back(propName); } if (!missingProperties.empty()) { reportError(location, MissingProperties{superTy, subTy, std::move(missingProperties)}); return; } } // And vice versa if we're invariant if (variance == Invariant && !superTable->indexer && superTable->state != TableState::Unsealed && superTable->state != TableState::Free) { for (const auto& [propName, subProp] : subTable->props) { auto superIter = superTable->props.find(propName); if (superIter == superTable->props.end()) extraProperties.push_back(propName); } if (!extraProperties.empty()) { reportError(location, MissingProperties{superTy, subTy, std::move(extraProperties), MissingProperties::Extra}); return; } } // Width subtyping: any property in the supertype must be in the subtype, // and the types must agree. for (const auto& [name, prop] : superTable->props) { const auto& r = subTable->props.find(name); if (r != subTable->props.end()) { // TODO: read-only properties don't need invariance Resetter resetter{&variance}; variance = Invariant; Unifier innerState = makeChildUnifier(); innerState.tryUnify_(r->second.type(), prop.type()); checkChildUnifierTypeMismatch(innerState.errors, name, superTy, subTy); if (innerState.errors.empty()) log.concat(std::move(innerState.log)); failure |= innerState.failure; } else if (subTable->indexer && maybeString(subTable->indexer->indexType)) { // TODO: read-only indexers don't need invariance // TODO: really we should only allow this if prop.type is optional. Resetter resetter{&variance}; variance = Invariant; Unifier innerState = makeChildUnifier(); innerState.tryUnify_(subTable->indexer->indexResultType, prop.type()); checkChildUnifierTypeMismatch(innerState.errors, name, superTy, subTy); if (innerState.errors.empty()) log.concat(std::move(innerState.log)); failure |= innerState.failure; } else if (subTable->state == TableState::Unsealed && isOptional(prop.type())) // This is sound because unsealed table types are precise, so `{ p : T } <: { p : T, q : U? }` // since if `t : { p : T }` then we are guaranteed that `t.q` is `nil`. // TODO: if the supertype is written to, the subtype may no longer be precise (alias analysis?) { } else if (subTable->state == TableState::Free) { PendingType* pendingSub = log.queue(activeSubTy); TableType* ttv = getMutable(pendingSub); LUAU_ASSERT(ttv); ttv->props[name] = prop; subTable = ttv; } else missingProperties.push_back(name); // 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 = log.follow(superTy); TypeId subTyNew = log.follow(activeSubTy); // If one of the types stopped being a table altogether, we need to restart from the top if ((superTy != superTyNew || activeSubTy != subTyNew) && errors.empty()) return tryUnify(subTy, superTy, false, isIntersection); // Otherwise, restart only the table unification TableType* newSuperTable = log.getMutable(superTyNew); TableType* newSubTable = log.getMutable(subTyNew); if (superTable != newSuperTable || subTable != newSubTable) { if (errors.empty()) return tryUnifyTables(subTy, superTy, isIntersection); else return; } } for (const auto& [name, prop] : subTable->props) { if (superTable->props.count(name)) { // If both lt and rt contain the property, then // we're done since we already unified them above } else if (superTable->indexer && maybeString(superTable->indexer->indexType)) { // TODO: read-only indexers don't need invariance // TODO: really we should only allow this if prop.type is optional. Resetter resetter{&variance}; variance = Invariant; Unifier innerState = makeChildUnifier(); innerState.tryUnify_(superTable->indexer->indexResultType, prop.type()); checkChildUnifierTypeMismatch(innerState.errors, name, superTy, subTy); if (innerState.errors.empty()) log.concat(std::move(innerState.log)); failure |= innerState.failure; } else if (superTable->state == TableState::Unsealed) { // TODO: this case is unsound when variance is Invariant, but without it lua-apps fails to typecheck. // TODO: file a JIRA // TODO: hopefully readonly/writeonly properties will fix this. Property clone = prop; clone.setType(deeplyOptional(clone.type())); PendingType* pendingSuper = log.queue(superTy); TableType* pendingSuperTtv = getMutable(pendingSuper); pendingSuperTtv->props[name] = clone; superTable = pendingSuperTtv; } else if (variance == Covariant) { } else if (superTable->state == TableState::Free) { PendingType* pendingSuper = log.queue(superTy); TableType* pendingSuperTtv = getMutable(pendingSuper); pendingSuperTtv->props[name] = prop; superTable = pendingSuperTtv; } else extraProperties.push_back(name); TypeId superTyNew = log.follow(superTy); TypeId subTyNew = log.follow(activeSubTy); // If one of the types stopped being a table altogether, we need to restart from the top if ((superTy != superTyNew || activeSubTy != 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. TableType* newSuperTable = log.getMutable(superTyNew); TableType* newSubTable = log.getMutable(subTyNew); if (superTable != newSuperTable || subTable != newSubTable) { if (errors.empty()) return tryUnifyTables(subTy, superTy, isIntersection); else return; } } // Unify indexers if (superTable->indexer && subTable->indexer) { // TODO: read-only indexers don't need invariance Resetter resetter{&variance}; variance = Invariant; Unifier innerState = makeChildUnifier(); innerState.tryUnify_(subTable->indexer->indexType, superTable->indexer->indexType); bool reported = !innerState.errors.empty(); checkChildUnifierTypeMismatch(innerState.errors, "[indexer key]", superTy, subTy); innerState.tryUnify_(subTable->indexer->indexResultType, superTable->indexer->indexResultType); if (!reported) checkChildUnifierTypeMismatch(innerState.errors, "[indexer value]", superTy, subTy); if (innerState.errors.empty()) log.concat(std::move(innerState.log)); failure |= innerState.failure; } else if (superTable->indexer) { if (subTable->state == TableState::Unsealed || subTable->state == TableState::Free) { // passing/assigning a table without an indexer to something that has one // e.g. table.insert(t, 1) where t is a non-sealed table and doesn't have an indexer. // TODO: we only need to do this if the supertype's indexer is read/write // since that can add indexed elements. log.changeIndexer(subTy, superTable->indexer); } } else if (subTable->indexer && variance == Invariant) { // Symmetric if we are invariant if (superTable->state == TableState::Unsealed || superTable->state == TableState::Free) { log.changeIndexer(superTy, subTable->indexer); } } // Changing the indexer can invalidate the table pointers. superTable = log.getMutable(log.follow(superTy)); subTable = log.getMutable(log.follow(activeSubTy)); if (!superTable || !subTable) return; if (!missingProperties.empty()) { reportError(location, MissingProperties{superTy, subTy, std::move(missingProperties)}); return; } if (!extraProperties.empty()) { reportError(location, MissingProperties{superTy, subTy, std::move(extraProperties), MissingProperties::Extra}); return; } /* * Types are commonly cyclic, so it is entirely possible * for unifying a property of a table to change the table itself! * We need to check for this and start over if we notice this occurring. * * I believe this is guaranteed to terminate eventually because this will * only happen when a free table is bound to another table. */ if (superTable->boundTo || subTable->boundTo) return tryUnify_(subTy, superTy); if (superTable->state == TableState::Free) { log.bindTable(superTy, subTy); } else if (subTable->state == TableState::Free) { log.bindTable(subTy, superTy); } } void Unifier::tryUnifyScalarShape(TypeId subTy, TypeId superTy, bool reversed) { TypeId osubTy = subTy; TypeId osuperTy = superTy; if (FFlag::LuauUninhabitedSubAnything2 && checkInhabited && !normalizer->isInhabited(subTy)) return; if (reversed) std::swap(subTy, superTy); TableType* superTable = log.getMutable(superTy); if (!superTable || superTable->state != TableState::Free) 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, mismatchContext()}); else reportError(location, TypeMismatch{osuperTy, osubTy, reason, mismatchContext()}); }; // Given t1 where t1 = { lower: (t1) -> (a, b...) } // It should be the case that `string <: t1` iff `(subtype's metatable).__index <: t1` if (auto metatable = getMetatable(subTy, builtinTypes)) { auto mttv = log.get(*metatable); if (!mttv) fail(std::nullopt); if (auto it = mttv->props.find("__index"); it != mttv->props.end()) { TypeId ty = it->second.type(); Unifier child = makeChildUnifier(); child.tryUnify_(ty, superTy); // 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 // Here we check if we have a new supertype instead of the original free table and try original subtype <: new supertype check TypeId newSuperTy = child.log.follow(superTy); if (superTy != newSuperTy && canUnify(subTy, newSuperTy).empty()) { log.replace(superTy, BoundType{subTy}); return; } if (auto e = hasUnificationTooComplex(child.errors)) reportError(*e); else if (!child.errors.empty()) fail(child.errors.front()); log.concat(std::move(child.log)); // 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 if (child.errors.empty()) log.replace(superTy, BoundType{subTy}); return; } else { return fail(std::nullopt); } } reportError(location, TypeMismatch{osuperTy, osubTy, mismatchContext()}); return; } TypeId Unifier::deeplyOptional(TypeId ty, std::unordered_map seen) { ty = follow(ty); if (isOptional(ty)) return ty; else if (const TableType* ttv = get(ty)) { TypeId& result = seen[ty]; if (result) return result; result = types->addType(*ttv); TableType* resultTtv = getMutable(result); for (auto& [name, prop] : resultTtv->props) prop.setType(deeplyOptional(prop.type(), seen)); return types->addType(UnionType{{builtinTypes->nilType, result}}); } else return types->addType(UnionType{{builtinTypes->nilType, ty}}); } void Unifier::tryUnifyWithMetatable(TypeId subTy, TypeId superTy, bool reversed) { const MetatableType* superMetatable = get(superTy); if (!superMetatable) ice("tryUnifyMetatable invoked with non-metatable Type"); TypeError mismatchError = TypeError{location, TypeMismatch{reversed ? subTy : superTy, reversed ? superTy : subTy, mismatchContext()}}; if (const MetatableType* subMetatable = log.getMutable(subTy)) { Unifier innerState = makeChildUnifier(); innerState.tryUnify_(subMetatable->table, superMetatable->table); innerState.tryUnify_(subMetatable->metatable, superMetatable->metatable); 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(), mismatchContext()}); log.concat(std::move(innerState.log)); failure |= innerState.failure; } else if (TableType* subTable = log.getMutable(subTy)) { switch (subTable->state) { case TableState::Free: { if (FFlag::DebugLuauDeferredConstraintResolution) { Unifier innerState = makeChildUnifier(); bool missingProperty = false; for (const auto& [propName, prop] : subTable->props) { if (std::optional mtPropTy = findTablePropertyRespectingMeta(superTy, propName)) { innerState.tryUnify(prop.type(), *mtPropTy); } else { reportError(mismatchError); missingProperty = true; break; } } if (const TableType* superTable = log.get(log.follow(superMetatable->table))) { // TODO: Unify indexers. } 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(), mismatchContext()}}); else if (!missingProperty) { log.concat(std::move(innerState.log)); log.bindTable(subTy, superTy); failure |= innerState.failure; } } else { tryUnify_(subTy, superMetatable->table); log.bindTable(subTy, superTy); } break; } // We know the shape of sealed, unsealed, and generic tables; you can't add a metatable on to any of these. case TableState::Sealed: case TableState::Unsealed: case TableState::Generic: reportError(mismatchError); } } else if (log.getMutable(subTy) || log.getMutable(subTy)) { } else { reportError(mismatchError); } } // Class unification is almost, but not quite symmetrical. We use the 'reversed' boolean to indicate which scenario we are evaluating. void Unifier::tryUnifyWithClass(TypeId subTy, TypeId superTy, bool reversed) { if (reversed) std::swap(superTy, subTy); auto fail = [&]() { if (!reversed) reportError(location, TypeMismatch{superTy, subTy, mismatchContext()}); else reportError(location, TypeMismatch{subTy, superTy, mismatchContext()}); }; const ClassType* superClass = get(superTy); if (!superClass) ice("tryUnifyClass invoked with non-class Type"); if (const ClassType* subClass = get(subTy)) { switch (variance) { case Covariant: if (!isSubclass(subClass, superClass)) return fail(); return; case Invariant: if (subClass != superClass) return fail(); return; } ice("Illegal variance setting!"); } else if (TableType* subTable = getMutable(subTy)) { /** * A free table is something whose shape we do not exactly know yet. * Thus, it is entirely reasonable that we might discover that it is being used as some class type. * In this case, the free table must indeed be that exact class. * For this to hold, the table must not have any properties that the class does not. * Further, all properties of the table should unify cleanly with the matching class properties. * TODO: What does it mean for the table to have an indexer? (probably failure?) * * Tables that are not free are known to be actual tables. */ if (subTable->state != TableState::Free) return fail(); bool ok = true; for (const auto& [propName, prop] : subTable->props) { const Property* classProp = lookupClassProp(superClass, propName); if (!classProp) { ok = false; reportError(location, UnknownProperty{superTy, propName}); } else { Unifier innerState = makeChildUnifier(); innerState.tryUnify_(classProp->type(), prop.type()); checkChildUnifierTypeMismatch(innerState.errors, propName, reversed ? subTy : superTy, reversed ? superTy : subTy); if (innerState.errors.empty()) { log.concat(std::move(innerState.log)); failure |= innerState.failure; } else { ok = false; } } } if (subTable->indexer) { ok = false; std::string msg = "Class " + superClass->name + " does not have an indexer"; reportError(location, GenericError{msg}); } if (!ok) return; log.bindTable(subTy, superTy); } else return fail(); } void Unifier::tryUnifyNegations(TypeId subTy, TypeId superTy) { if (!log.get(subTy) && !log.get(superTy)) ice("tryUnifyNegations superTy or subTy must be a negation type"); // We cannot normalize a type that contains blocked types. We have to // stop for now if we find any. if (!FFlag::LuauNormalizeBlockedTypes && DEPRECATED_blockOnBlockedTypes(subTy, superTy)) return; const NormalizedType* subNorm = normalizer->normalize(subTy); const NormalizedType* superNorm = normalizer->normalize(superTy); if (!subNorm || !superNorm) return reportError(location, UnificationTooComplex{}); // T & queue, DenseHashSet& seenTypePacks, Unifier& state, TypePackId a, TypePackId anyTypePack) { while (true) { a = state.log.follow(a); if (seenTypePacks.find(a)) break; seenTypePacks.insert(a); if (state.log.getMutable(a)) { state.log.replace(a, BoundTypePack{anyTypePack}); } else if (auto tp = state.log.getMutable(a)) { queue.insert(queue.end(), tp->head.begin(), tp->head.end()); if (tp->tail) a = *tp->tail; else break; } } } void Unifier::tryUnifyVariadics(TypePackId subTp, TypePackId superTp, bool reversed, int subOffset) { const VariadicTypePack* superVariadic = log.getMutable(superTp); const TypeId variadicTy = follow(superVariadic->ty); if (!superVariadic) ice("passed non-variadic pack to tryUnifyVariadics"); if (const VariadicTypePack* subVariadic = log.get(subTp)) { tryUnify_(reversed ? variadicTy : subVariadic->ty, reversed ? subVariadic->ty : variadicTy); } else if (log.get(subTp)) { TypePackIterator subIter = begin(subTp, &log); TypePackIterator subEnd = end(subTp); std::advance(subIter, subOffset); while (subIter != subEnd) { tryUnify_(reversed ? variadicTy : *subIter, reversed ? *subIter : variadicTy); ++subIter; } if (std::optional maybeTail = subIter.tail()) { TypePackId tail = follow(*maybeTail); if (get(tail)) { log.replace(tail, BoundTypePack(superTp)); } else if (const VariadicTypePack* vtp = get(tail)) { tryUnify_(vtp->ty, variadicTy); } else if (get(tail)) { reportError(location, GenericError{"Cannot unify variadic and generic packs"}); } else if (get(tail)) { // Nothing to do here. } else { ice("Unknown TypePack kind"); } } } else if (FFlag::LuauVariadicAnyCanBeGeneric && get(variadicTy) && log.get(subTp)) { // Nothing to do. This is ok. } else { reportError(location, GenericError{"Failed to unify variadic packs"}); } } static void tryUnifyWithAny(std::vector& queue, Unifier& state, DenseHashSet& seen, DenseHashSet& seenTypePacks, const TypeArena* typeArena, TypeId anyType, TypePackId anyTypePack) { while (!queue.empty()) { TypeId ty = state.log.follow(queue.back()); queue.pop_back(); // Types from other modules don't have free types if (ty->owningArena != typeArena) continue; if (seen.find(ty)) continue; seen.insert(ty); if (state.log.getMutable(ty)) { // TODO: Only bind if the anyType isn't any, unknown, or error (?) state.log.replace(ty, BoundType{anyType}); } else if (auto fun = state.log.getMutable(ty)) { queueTypePack(queue, seenTypePacks, state, fun->argTypes, anyTypePack); queueTypePack(queue, seenTypePacks, state, fun->retTypes, anyTypePack); } else if (auto table = state.log.getMutable(ty)) { for (const auto& [_name, prop] : table->props) queue.push_back(prop.type()); if (table->indexer) { queue.push_back(table->indexer->indexType); queue.push_back(table->indexer->indexResultType); } } else if (auto mt = state.log.getMutable(ty)) { queue.push_back(mt->table); queue.push_back(mt->metatable); } else if (state.log.getMutable(ty)) { // ClassTypes never contain free types. } else if (auto union_ = state.log.getMutable(ty)) queue.insert(queue.end(), union_->options.begin(), union_->options.end()); else if (auto intersection = state.log.getMutable(ty)) queue.insert(queue.end(), intersection->parts.begin(), intersection->parts.end()); else { } // Primitives, any, errors, and generics are left untouched. } } void Unifier::tryUnifyWithAny(TypeId subTy, TypeId anyTy) { LUAU_ASSERT(get(anyTy) || get(anyTy) || get(anyTy) || get(anyTy)); // These types are not visited in general loop below if (log.get(subTy) || log.get(subTy) || log.get(subTy)) return; TypePackId anyTp = types->addTypePack(TypePackVar{VariadicTypePack{anyTy}}); std::vector queue = {subTy}; sharedState.tempSeenTy.clear(); sharedState.tempSeenTp.clear(); Luau::tryUnifyWithAny(queue, *this, sharedState.tempSeenTy, sharedState.tempSeenTp, types, anyTy, anyTp); } void Unifier::tryUnifyWithAny(TypePackId subTy, TypePackId anyTp) { LUAU_ASSERT(get(anyTp)); const TypeId anyTy = builtinTypes->errorRecoveryType(); std::vector queue; sharedState.tempSeenTy.clear(); sharedState.tempSeenTp.clear(); queueTypePack(queue, sharedState.tempSeenTp, *this, subTy, anyTp); Luau::tryUnifyWithAny(queue, *this, sharedState.tempSeenTy, sharedState.tempSeenTp, types, anyTy, anyTp); } std::optional Unifier::findTablePropertyRespectingMeta(TypeId lhsType, Name name) { return Luau::findTablePropertyRespectingMeta(builtinTypes, 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, bool reversed) { sharedState.tempSeenTy.clear(); bool occurs = occursCheck(sharedState.tempSeenTy, needle, haystack); if (occurs && FFlag::LuauOccursIsntAlwaysFailure) { Unifier innerState = makeChildUnifier(); if (const UnionType* ut = get(haystack)) { if (reversed) innerState.tryUnifyUnionWithType(haystack, ut, needle); else innerState.tryUnifyTypeWithUnion(needle, haystack, ut, /* cacheEnabled = */ false, /* isFunction = */ false); } else if (const IntersectionType* it = get(haystack)) { if (reversed) innerState.tryUnifyIntersectionWithType(haystack, it, needle, /* cacheEnabled = */ false, /* isFunction = */ false); else innerState.tryUnifyTypeWithIntersection(needle, haystack, it); } else { innerState.failure = true; } if (innerState.failure) { reportError(location, OccursCheckFailed{}); log.replace(needle, *builtinTypes->errorRecoveryType()); } } return occurs; } bool Unifier::occursCheck(DenseHashSet& seen, TypeId needle, TypeId haystack) { RecursionLimiter _ra(&sharedState.counters.recursionCount, sharedState.counters.recursionLimit); bool occurrence = false; auto check = [&](TypeId tv) { if (occursCheck(seen, needle, tv)) occurrence = true; }; needle = log.follow(needle); haystack = log.follow(haystack); if (seen.find(haystack)) return false; seen.insert(haystack); if (log.getMutable(needle)) return false; if (!log.getMutable(needle)) ice("Expected needle to be free"); if (needle == haystack) { if (!FFlag::LuauOccursIsntAlwaysFailure) { reportError(location, OccursCheckFailed{}); log.replace(needle, *builtinTypes->errorRecoveryType()); } return true; } if (log.getMutable(haystack)) return false; else if (auto a = log.getMutable(haystack)) { for (TypeId ty : a->options) check(ty); } else if (auto a = log.getMutable(haystack)) { for (TypeId ty : a->parts) check(ty); } return occurrence; } bool Unifier::occursCheck(TypePackId needle, TypePackId haystack, bool reversed) { sharedState.tempSeenTp.clear(); bool occurs = occursCheck(sharedState.tempSeenTp, needle, haystack); if (occurs && FFlag::LuauOccursIsntAlwaysFailure) { reportError(location, OccursCheckFailed{}); log.replace(needle, *builtinTypes->errorRecoveryTypePack()); } return occurs; } bool Unifier::occursCheck(DenseHashSet& seen, TypePackId needle, TypePackId haystack) { needle = log.follow(needle); haystack = log.follow(haystack); if (seen.find(haystack)) return false; seen.insert(haystack); if (log.getMutable(needle)) return false; if (!log.getMutable(needle)) ice("Expected needle pack to be free"); RecursionLimiter _ra(&sharedState.counters.recursionCount, sharedState.counters.recursionLimit); while (!log.getMutable(haystack)) { if (needle == haystack) { if (!FFlag::LuauOccursIsntAlwaysFailure) { reportError(location, OccursCheckFailed{}); log.replace(needle, *builtinTypes->errorRecoveryTypePack()); } return true; } if (auto a = get(haystack); a && a->tail) { haystack = log.follow(*a->tail); continue; } break; } return false; } Unifier Unifier::makeChildUnifier() { Unifier u = Unifier{normalizer, mode, scope, location, variance, &log}; u.normalize = normalize; u.checkInhabited = checkInhabited; u.useScopes = useScopes; return u; } // A utility function that appends the given error to the unifier's error log. // This allows setting a breakpoint wherever the unifier reports an error. // // Note: report error accepts its arguments by value intentionally to reduce the stack usage of functions which call `reportError`. void Unifier::reportError(Location location, TypeErrorData data) { errors.emplace_back(std::move(location), std::move(data)); failure = true; } // A utility function that appends the given error to the unifier's error log. // This allows setting a breakpoint wherever the unifier reports an error. // // Note: to conserve stack space in calling functions it is generally preferred to call `Unifier::reportError(Location location, TypeErrorData data)` // instead of this method. void Unifier::reportError(TypeError err) { errors.push_back(std::move(err)); failure = true; } bool Unifier::isNonstrictMode() const { return (mode == Mode::Nonstrict) || (mode == Mode::NoCheck); } void Unifier::checkChildUnifierTypeMismatch(const ErrorVec& innerErrors, TypeId wantedType, TypeId givenType) { if (auto e = hasUnificationTooComplex(innerErrors)) reportError(*e); else if (!innerErrors.empty()) reportError(location, TypeMismatch{wantedType, givenType, mismatchContext()}); } void Unifier::checkChildUnifierTypeMismatch(const ErrorVec& innerErrors, const std::string& prop, TypeId wantedType, TypeId givenType) { 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(), mismatchContext()}}); } void Unifier::ice(const std::string& message, const Location& location) { sharedState.iceHandler->ice(message, location); } void Unifier::ice(const std::string& message) { sharedState.iceHandler->ice(message); } } // namespace Luau