// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/TypeFamily.h" #include "Luau/DenseHash.h" #include "Luau/VisitType.h" #include "Luau/TxnLog.h" #include "Luau/Substitution.h" #include "Luau/ToString.h" #include "Luau/TypeUtils.h" #include "Luau/Unifier.h" #include "Luau/Instantiation.h" #include "Luau/Normalize.h" LUAU_DYNAMIC_FASTINTVARIABLE(LuauTypeFamilyGraphReductionMaximumSteps, 1'000'000); namespace Luau { struct InstanceCollector : TypeOnceVisitor { std::deque tys; std::deque tps; bool visit(TypeId ty, const TypeFamilyInstanceType&) override { // TypeOnceVisitor performs a depth-first traversal in the absence of // cycles. This means that by pushing to the front of the queue, we will // try to reduce deeper instances first if we start with the first thing // in the queue. Consider Add, number>, number>: // we want to reduce the innermost Add instantiation // first. tys.push_front(ty); return true; } bool visit(TypeId ty, const ClassType&) override { return false; } bool visit(TypePackId tp, const TypeFamilyInstanceTypePack&) override { // TypeOnceVisitor performs a depth-first traversal in the absence of // cycles. This means that by pushing to the front of the queue, we will // try to reduce deeper instances first if we start with the first thing // in the queue. Consider Add, number>, number>: // we want to reduce the innermost Add instantiation // first. tps.push_front(tp); return true; } }; struct FamilyReducer { std::deque queuedTys; std::deque queuedTps; DenseHashSet irreducible{nullptr}; FamilyGraphReductionResult result; Location location; NotNull arena; NotNull builtins; TxnLog* parentLog = nullptr; TxnLog log; bool force = false; NotNull scope; NotNull normalizer; FamilyReducer(std::deque queuedTys, std::deque queuedTps, Location location, NotNull arena, NotNull builtins, NotNull scope, NotNull normalizer, TxnLog* parentLog = nullptr, bool force = false) : queuedTys(std::move(queuedTys)) , queuedTps(std::move(queuedTps)) , location(location) , arena(arena) , builtins(builtins) , parentLog(parentLog) , log(parentLog) , force(force) , scope(scope) , normalizer(normalizer) { } enum class SkipTestResult { Irreducible, Defer, Okay, }; SkipTestResult testForSkippability(TypeId ty) { ty = log.follow(ty); if (log.is(ty)) { if (!irreducible.contains(ty)) return SkipTestResult::Defer; else return SkipTestResult::Irreducible; } else if (log.is(ty)) { return SkipTestResult::Irreducible; } return SkipTestResult::Okay; } SkipTestResult testForSkippability(TypePackId ty) { ty = log.follow(ty); if (log.is(ty)) { if (!irreducible.contains(ty)) return SkipTestResult::Defer; else return SkipTestResult::Irreducible; } else if (log.is(ty)) { return SkipTestResult::Irreducible; } return SkipTestResult::Okay; } template void replace(T subject, T replacement) { if (parentLog) parentLog->replace(subject, Unifiable::Bound{replacement}); else asMutable(subject)->ty.template emplace>(replacement); if constexpr (std::is_same_v) result.reducedTypes.insert(subject); else if constexpr (std::is_same_v) result.reducedPacks.insert(subject); } template void handleFamilyReduction(T subject, TypeFamilyReductionResult reduction) { if (reduction.result) replace(subject, *reduction.result); else { irreducible.insert(subject); if (reduction.uninhabited || force) { if constexpr (std::is_same_v) result.errors.push_back(TypeError{location, UninhabitedTypeFamily{subject}}); else if constexpr (std::is_same_v) result.errors.push_back(TypeError{location, UninhabitedTypePackFamily{subject}}); } else if (!reduction.uninhabited && !force) { for (TypeId b : reduction.blockedTypes) result.blockedTypes.insert(b); for (TypePackId b : reduction.blockedPacks) result.blockedPacks.insert(b); } } } bool done() { return queuedTys.empty() && queuedTps.empty(); } template bool testParameters(T subject, const I* tfit) { for (TypeId p : tfit->typeArguments) { SkipTestResult skip = testForSkippability(p); if (skip == SkipTestResult::Irreducible) { irreducible.insert(subject); return false; } else if (skip == SkipTestResult::Defer) { if constexpr (std::is_same_v) queuedTys.push_back(subject); else if constexpr (std::is_same_v) queuedTps.push_back(subject); return false; } } for (TypePackId p : tfit->packArguments) { SkipTestResult skip = testForSkippability(p); if (skip == SkipTestResult::Irreducible) { irreducible.insert(subject); return false; } else if (skip == SkipTestResult::Defer) { if constexpr (std::is_same_v) queuedTys.push_back(subject); else if constexpr (std::is_same_v) queuedTps.push_back(subject); return false; } } return true; } void stepType() { TypeId subject = log.follow(queuedTys.front()); queuedTys.pop_front(); if (irreducible.contains(subject)) return; if (const TypeFamilyInstanceType* tfit = log.get(subject)) { if (!testParameters(subject, tfit)) return; TypeFamilyReductionResult result = tfit->family->reducer(tfit->typeArguments, tfit->packArguments, arena, builtins, NotNull{&log}, scope, normalizer); handleFamilyReduction(subject, result); } } void stepPack() { TypePackId subject = log.follow(queuedTps.front()); queuedTps.pop_front(); if (irreducible.contains(subject)) return; if (const TypeFamilyInstanceTypePack* tfit = log.get(subject)) { if (!testParameters(subject, tfit)) return; TypeFamilyReductionResult result = tfit->family->reducer(tfit->typeArguments, tfit->packArguments, arena, builtins, NotNull{&log}, scope, normalizer); handleFamilyReduction(subject, result); } } void step() { if (!queuedTys.empty()) stepType(); else if (!queuedTps.empty()) stepPack(); } }; static FamilyGraphReductionResult reduceFamiliesInternal(std::deque queuedTys, std::deque queuedTps, Location location, NotNull arena, NotNull builtins, NotNull scope, NotNull normalizer, TxnLog* log, bool force) { FamilyReducer reducer{std::move(queuedTys), std::move(queuedTps), location, arena, builtins, scope, normalizer, log, force}; int iterationCount = 0; while (!reducer.done()) { reducer.step(); ++iterationCount; if (iterationCount > DFInt::LuauTypeFamilyGraphReductionMaximumSteps) { reducer.result.errors.push_back(TypeError{location, CodeTooComplex{}}); break; } } return std::move(reducer.result); } FamilyGraphReductionResult reduceFamilies(TypeId entrypoint, Location location, NotNull arena, NotNull builtins, NotNull scope, NotNull normalizer, TxnLog* log, bool force) { InstanceCollector collector; try { collector.traverse(entrypoint); } catch (RecursionLimitException&) { return FamilyGraphReductionResult{}; } if (collector.tys.empty() && collector.tps.empty()) return {}; return reduceFamiliesInternal(std::move(collector.tys), std::move(collector.tps), location, arena, builtins, scope, normalizer, log, force); } FamilyGraphReductionResult reduceFamilies(TypePackId entrypoint, Location location, NotNull arena, NotNull builtins, NotNull scope, NotNull normalizer, TxnLog* log, bool force) { InstanceCollector collector; try { collector.traverse(entrypoint); } catch (RecursionLimitException&) { return FamilyGraphReductionResult{}; } if (collector.tys.empty() && collector.tps.empty()) return {}; return reduceFamiliesInternal(std::move(collector.tys), std::move(collector.tps), location, arena, builtins, scope, normalizer, log, force); } bool isPending(TypeId ty, NotNull log) { return log->is(ty) || log->is(ty) || log->is(ty) || log->is(ty); } TypeFamilyReductionResult addFamilyFn(std::vector typeParams, std::vector packParams, NotNull arena, NotNull builtins, NotNull log, NotNull scope, NotNull normalizer) { if (typeParams.size() != 2 || !packParams.empty()) { // TODO: ICE? LUAU_ASSERT(false); return {std::nullopt, true, {}, {}}; } TypeId lhsTy = log->follow(typeParams.at(0)); TypeId rhsTy = log->follow(typeParams.at(1)); const NormalizedType* normLhsTy = normalizer->normalize(lhsTy); const NormalizedType* normRhsTy = normalizer->normalize(rhsTy); if (normLhsTy && normRhsTy && normLhsTy->isNumber() && normRhsTy->isNumber()) { return {builtins->numberType, false, {}, {}}; } else if (log->is(lhsTy) || log->is(rhsTy)) { return {builtins->anyType, false, {}, {}}; } else if (log->is(lhsTy) || log->is(rhsTy)) { return {builtins->errorRecoveryType(), false, {}, {}}; } else if (log->is(lhsTy) || log->is(rhsTy)) { return {builtins->neverType, false, {}, {}}; } else if (isPending(lhsTy, log)) { return {std::nullopt, false, {lhsTy}, {}}; } else if (isPending(rhsTy, log)) { return {std::nullopt, false, {rhsTy}, {}}; } // findMetatableEntry demands the ability to emit errors, so we must give it // the necessary state to do that, even if we intend to just eat the errors. ErrorVec dummy; std::optional addMm = findMetatableEntry(builtins, dummy, lhsTy, "__add", Location{}); bool reversed = false; if (!addMm) { addMm = findMetatableEntry(builtins, dummy, rhsTy, "__add", Location{}); reversed = true; } if (!addMm) return {std::nullopt, true, {}, {}}; if (isPending(log->follow(*addMm), log)) return {std::nullopt, false, {log->follow(*addMm)}, {}}; const FunctionType* mmFtv = log->get(log->follow(*addMm)); if (!mmFtv) return {std::nullopt, true, {}, {}}; Instantiation instantiation{log.get(), arena.get(), TypeLevel{}, scope.get()}; if (std::optional instantiatedAddMm = instantiation.substitute(log->follow(*addMm))) { if (const FunctionType* instantiatedMmFtv = get(*instantiatedAddMm)) { std::vector inferredArgs; if (!reversed) inferredArgs = {lhsTy, rhsTy}; else inferredArgs = {rhsTy, lhsTy}; TypePackId inferredArgPack = arena->addTypePack(std::move(inferredArgs)); Unifier u{normalizer, scope, Location{}, Variance::Covariant, log.get()}; u.tryUnify(inferredArgPack, instantiatedMmFtv->argTypes); if (std::optional ret = first(instantiatedMmFtv->retTypes); ret && u.errors.empty()) { return {u.log.follow(*ret), false, {}, {}}; } else { return {std::nullopt, true, {}, {}}; } } else { return {builtins->errorRecoveryType(), false, {}, {}}; } } else { // TODO: Not the nicest logic here. return {std::nullopt, true, {}, {}}; } } BuiltinTypeFamilies::BuiltinTypeFamilies() : addFamily{"Add", addFamilyFn} { } } // namespace Luau