// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/TypeReduction.h" #include "Luau/Common.h" #include "Luau/Error.h" #include "Luau/RecursionCounter.h" #include #include LUAU_FASTINTVARIABLE(LuauTypeReductionCartesianProductLimit, 100'000) LUAU_FASTINTVARIABLE(LuauTypeReductionRecursionLimit, 700) LUAU_FASTFLAGVARIABLE(DebugLuauDontReduceTypes, false) namespace Luau { namespace { struct RecursionGuard : RecursionLimiter { std::deque* seen; RecursionGuard(int* count, int limit, std::deque* seen) : RecursionLimiter(count, limit) , seen(seen) { // count has been incremented, which should imply that seen has already had an element pushed in. LUAU_ASSERT(size_t(*count) == seen->size()); } ~RecursionGuard() { LUAU_ASSERT(!seen->empty()); // It is UB to pop_back() on an empty deque. seen->pop_back(); } }; template std::pair get2(const Thing& one, const Thing& two) { const A* a = get(one); const B* b = get(two); return a && b ? std::make_pair(a, b) : std::make_pair(nullptr, nullptr); } struct TypeReducer { NotNull arena; NotNull builtinTypes; NotNull handle; std::unordered_map copies; std::deque seen; int depth = 0; // When we encounter _any type_ that which is usually mutated in-place, we need to not cache the result. // e.g. `'a & {} T` may have an upper bound constraint `{}` placed upon `'a`, but this constraint was not // known when we decided to reduce this intersection type. By not caching, we'll always be forced to perform // the reduction calculus over again. bool cacheOk = true; TypeId reduce(TypeId ty); TypePackId reduce(TypePackId tp); std::optional intersectionType(TypeId left, TypeId right); std::optional unionType(TypeId left, TypeId right); TypeId tableType(TypeId ty); TypeId functionType(TypeId ty); TypeId negationType(TypeId ty); RecursionGuard guard(TypeId ty); RecursionGuard guard(TypePackId tp); void checkCacheable(TypeId ty); void checkCacheable(TypePackId tp); template LUAU_NOINLINE std::pair copy(TypeId ty, const T* t) { if (auto it = copies.find(ty); it != copies.end()) return {it->second, getMutable(it->second)}; TypeId copiedTy = arena->addType(*t); copies[ty] = copiedTy; return {copiedTy, getMutable(copiedTy)}; } using Folder = std::optional (TypeReducer::*)(TypeId, TypeId); template void foldl_impl(Iter it, Iter endIt, Folder f, NotNull> result) { while (it != endIt) { bool replaced = false; TypeId currentTy = reduce(*it); RecursionGuard rg = guard(*it); // We're hitting a case where the `currentTy` returned a type that's the same as `T`. // e.g. `(string?) & ~(false | nil)` became `(string?) & (~false & ~nil)` but the current iterator we're consuming doesn't know this. // We will need to recurse and traverse that first. if (auto t = get(currentTy)) { foldl_impl(begin(t), end(t), f, result); ++it; continue; } auto resultIt = result->begin(); while (resultIt != result->end()) { TypeId& ty = *resultIt; std::optional reduced = (this->*f)(ty, currentTy); if (reduced && replaced) { // We want to erase any other elements that occurs after the first replacement too. // e.g. `"a" | "b" | string` where `"a"` and `"b"` is in the `result` vector, then `string` replaces both `"a"` and `"b"`. // If we don't erase redundant elements, `"b"` may be kept or be replaced by `string`, leaving us with `string | string`. resultIt = result->erase(resultIt); } else if (reduced && !replaced) { ++resultIt; replaced = true; ty = *reduced; } else { ++resultIt; continue; } } if (!replaced) result->push_back(currentTy); ++it; } } template TypeId foldl(Iter it, Iter endIt, Folder f) { std::vector result; foldl_impl(it, endIt, f, NotNull{&result}); if (result.size() == 1) return result[0]; else return arena->addType(T{std::move(result)}); } }; TypeId TypeReducer::reduce(TypeId ty) { ty = follow(ty); if (std::find(seen.begin(), seen.end(), ty) != seen.end()) return ty; RecursionGuard rg = guard(ty); checkCacheable(ty); if (auto i = get(ty)) return foldl(begin(i), end(i), &TypeReducer::intersectionType); else if (auto u = get(ty)) return foldl(begin(u), end(u), &TypeReducer::unionType); else if (get(ty) || get(ty)) return tableType(ty); else if (get(ty)) return functionType(ty); else if (auto n = get(ty)) return negationType(follow(n->ty)); else return ty; } TypePackId TypeReducer::reduce(TypePackId tp) { tp = follow(tp); if (std::find(seen.begin(), seen.end(), tp) != seen.end()) return tp; RecursionGuard rg = guard(tp); checkCacheable(tp); TypePackIterator it = begin(tp); std::vector head; while (it != end(tp)) { head.push_back(reduce(*it)); ++it; } std::optional tail = it.tail(); if (tail) { if (auto vtp = get(follow(*it.tail()))) tail = arena->addTypePack(VariadicTypePack{reduce(vtp->ty), vtp->hidden}); } return arena->addTypePack(TypePack{std::move(head), tail}); } std::optional TypeReducer::intersectionType(TypeId left, TypeId right) { LUAU_ASSERT(!get(left)); LUAU_ASSERT(!get(right)); if (get(left)) return left; // never & T ~ never else if (get(right)) return right; // T & never ~ never else if (get(left)) return right; // unknown & T ~ T else if (get(right)) return left; // T & unknown ~ T else if (get(left)) return right; // any & T ~ T else if (get(right)) return left; // T & any ~ T else if (get(left)) return std::nullopt; // 'a & T ~ 'a & T else if (get(right)) return std::nullopt; // T & 'a ~ T & 'a else if (get(left)) return std::nullopt; // G & T ~ G & T else if (get(right)) return std::nullopt; // T & G ~ T & G else if (get(left)) return std::nullopt; // error & T ~ error & T else if (get(right)) return std::nullopt; // T & error ~ T & error else if (auto ut = get(left)) { std::vector options; for (TypeId option : ut) { if (auto result = intersectionType(option, right)) options.push_back(*result); else options.push_back(arena->addType(IntersectionType{{option, right}})); } return foldl(begin(options), end(options), &TypeReducer::unionType); // (A | B) & T ~ (A & T) | (B & T) } else if (get(right)) return intersectionType(right, left); // T & (A | B) ~ (A | B) & T else if (auto [p1, p2] = get2(left, right); p1 && p2) { if (p1->type == p2->type) return left; // P1 & P2 ~ P1 iff P1 == P2 else return builtinTypes->neverType; // P1 & P2 ~ never iff P1 != P2 } else if (auto [p, s] = get2(left, right); p && s) { if (p->type == PrimitiveType::String && get(s)) return right; // string & "A" ~ "A" else if (p->type == PrimitiveType::Boolean && get(s)) return right; // boolean & true ~ true else return builtinTypes->neverType; // string & true ~ never } else if (auto [s, p] = get2(left, right); s && p) return intersectionType(right, left); // S & P ~ P & S else if (auto [p, f] = get2(left, right); p && f) { if (p->type == PrimitiveType::Function) return right; // function & () -> () ~ () -> () else return builtinTypes->neverType; // string & () -> () ~ never } else if (auto [f, p] = get2(left, right); f && p) return intersectionType(right, left); // () -> () & P ~ P & () -> () else if (auto [s1, s2] = get2(left, right); s1 && s2) { if (*s1 == *s2) return left; // "a" & "a" ~ "a" else return builtinTypes->neverType; // "a" & "b" ~ never } else if (auto [c1, c2] = get2(left, right); c1 && c2) { if (isSubclass(c1, c2)) return left; // Derived & Base ~ Derived else if (isSubclass(c2, c1)) return right; // Base & Derived ~ Derived else return builtinTypes->neverType; // Base & Unrelated ~ never } else if (auto [f1, f2] = get2(left, right); f1 && f2) { if (std::find(seen.begin(), seen.end(), left) != seen.end()) return std::nullopt; else if (std::find(seen.begin(), seen.end(), right) != seen.end()) return std::nullopt; return std::nullopt; // TODO } else if (auto [t1, t2] = get2(left, right); t1 && t2) { if (t1->state == TableState::Free || t2->state == TableState::Free) return std::nullopt; // '{ x: T } & { x: U } ~ '{ x: T } & { x: U } else if (t1->state == TableState::Generic || t2->state == TableState::Generic) return std::nullopt; // '{ x: T } & { x: U } ~ '{ x: T } & { x: U } if (std::find(seen.begin(), seen.end(), left) != seen.end()) return std::nullopt; else if (std::find(seen.begin(), seen.end(), right) != seen.end()) return std::nullopt; TypeId resultTy = arena->addType(TableType{}); TableType* table = getMutable(resultTy); table->state = t1->state == TableState::Sealed || t2->state == TableState::Sealed ? TableState::Sealed : TableState::Unsealed; for (const auto& [name, prop] : t1->props) { // TODO: when t1 has properties, we should also intersect that with the indexer in t2 if it exists, // even if we have the corresponding property in the other one. if (auto other = t2->props.find(name); other != t2->props.end()) { std::vector parts{prop.type, other->second.type}; TypeId propTy = foldl(begin(parts), end(parts), &TypeReducer::intersectionType); if (get(propTy)) return builtinTypes->neverType; // { p : string } & { p : number } ~ { p : string & number } ~ { p : never } ~ never else table->props[name] = {propTy}; // { p : string } & { p : ~"a" } ~ { p : string & ~"a" } } else table->props[name] = prop; // { p : string } & {} ~ { p : string } } for (const auto& [name, prop] : t2->props) { // TODO: And vice versa, t2 properties against t1 indexer if it exists, // even if we have the corresponding property in the other one. if (!t1->props.count(name)) table->props[name] = prop; // {} & { p : string } ~ { p : string } } if (t1->indexer && t2->indexer) { std::vector keyParts{t1->indexer->indexType, t2->indexer->indexType}; TypeId keyTy = foldl(begin(keyParts), end(keyParts), &TypeReducer::intersectionType); if (get(keyTy)) return builtinTypes->neverType; // { [string]: _ } & { [number]: _ } ~ { [string & number]: _ } ~ { [never]: _ } ~ never std::vector valueParts{t1->indexer->indexResultType, t2->indexer->indexResultType}; TypeId valueTy = foldl(begin(valueParts), end(valueParts), &TypeReducer::intersectionType); if (get(valueTy)) return builtinTypes->neverType; // { [_]: string } & { [_]: number } ~ { [_]: string & number } ~ { [_]: never } ~ never table->indexer = TableIndexer{keyTy, valueTy}; } else if (t1->indexer) table->indexer = t1->indexer; // { [number]: boolean } & { p : string } ~ { p : string, [number]: boolean } else if (t2->indexer) table->indexer = t2->indexer; // { p : string } & { [number]: boolean } ~ { p : string, [number]: boolean } return resultTy; } else if (auto [mt, tt] = get2(left, right); mt && tt) return std::nullopt; // TODO else if (auto [tt, mt] = get2(left, right); tt && mt) return intersectionType(right, left); // T & M ~ M & T else if (auto [m1, m2] = get2(left, right); m1 && m2) return std::nullopt; // TODO else if (auto nl = get(left)) { // These should've been reduced already. TypeId nlTy = follow(nl->ty); LUAU_ASSERT(!get(nlTy)); LUAU_ASSERT(!get(nlTy)); LUAU_ASSERT(!get(nlTy)); LUAU_ASSERT(!get(nlTy)); LUAU_ASSERT(!get(nlTy)); if (auto [np, p] = get2(nlTy, right); np && p) { if (np->type == p->type) return builtinTypes->neverType; // ~P1 & P2 ~ never iff P1 == P2 else return right; // ~P1 & P2 ~ P2 iff P1 != P2 } else if (auto [ns, s] = get2(nlTy, right); ns && s) { if (*ns == *s) return builtinTypes->neverType; // ~"A" & "A" ~ never else return right; // ~"A" & "B" ~ "B" } else if (auto [ns, p] = get2(nlTy, right); ns && p) { if (get(ns) && p->type == PrimitiveType::String) return std::nullopt; // ~"A" & string ~ ~"A" & string else if (get(ns) && p->type == PrimitiveType::Boolean) { // Because booleans contain a fixed amount of values (2), we can do something cooler with this one. const BooleanSingleton* b = get(ns); return arena->addType(SingletonType{BooleanSingleton{!b->value}}); // ~false & boolean ~ true } else return right; // ~"A" & number ~ number } else if (auto [np, s] = get2(nlTy, right); np && s) { if (np->type == PrimitiveType::String && get(s)) return builtinTypes->neverType; // ~string & "A" ~ never else if (np->type == PrimitiveType::Boolean && get(s)) return builtinTypes->neverType; // ~boolean & true ~ never else return right; // ~P & "A" ~ "A" } else if (auto [np, f] = get2(nlTy, right); np && f) { if (np->type == PrimitiveType::Function) return builtinTypes->neverType; // ~function & () -> () ~ never else return right; // ~string & () -> () ~ () -> () } else if (auto [nc, c] = get2(nlTy, right); nc && c) { if (isSubclass(nc, c)) return std::nullopt; // ~Derived & Base ~ ~Derived & Base else if (isSubclass(c, nc)) return builtinTypes->neverType; // ~Base & Derived ~ never else return right; // ~Base & Unrelated ~ Unrelated } else return std::nullopt; // TODO } else if (get(right)) return intersectionType(right, left); // T & ~U ~ ~U & T else return builtinTypes->neverType; // for all T and U except the ones handled above, T & U ~ never } std::optional TypeReducer::unionType(TypeId left, TypeId right) { LUAU_ASSERT(!get(left)); LUAU_ASSERT(!get(right)); if (get(left)) return right; // never | T ~ T else if (get(right)) return left; // T | never ~ T else if (get(left)) return left; // unknown | T ~ unknown else if (get(right)) return right; // T | unknown ~ unknown else if (get(left)) return left; // any | T ~ any else if (get(right)) return right; // T | any ~ any else if (get(left)) return std::nullopt; // error | T ~ error | T else if (get(right)) return std::nullopt; // T | error ~ T | error else if (auto [p1, p2] = get2(left, right); p1 && p2) { if (p1->type == p2->type) return left; // P1 | P2 ~ P1 iff P1 == P2 else return std::nullopt; // P1 | P2 ~ P1 | P2 iff P1 != P2 } else if (auto [p, s] = get2(left, right); p && s) { if (p->type == PrimitiveType::String && get(s)) return left; // string | "A" ~ string else if (p->type == PrimitiveType::Boolean && get(s)) return left; // boolean | true ~ boolean else return std::nullopt; // string | true ~ string | true } else if (auto [s, p] = get2(left, right); s && p) return unionType(right, left); // S | P ~ P | S else if (auto [p, f] = get2(left, right); p && f) { if (p->type == PrimitiveType::Function) return left; // function | () -> () ~ function else return std::nullopt; // P | () -> () ~ P | () -> () } else if (auto [f, p] = get2(left, right); f && p) return unionType(right, left); // () -> () | P ~ P | () -> () else if (auto [s1, s2] = get2(left, right); s1 && s2) { if (*s1 == *s2) return left; // "a" | "a" ~ "a" else return std::nullopt; // "a" | "b" ~ "a" | "b" } else if (auto [c1, c2] = get2(left, right); c1 && c2) { if (isSubclass(c1, c2)) return right; // Derived | Base ~ Base else if (isSubclass(c2, c1)) return left; // Base | Derived ~ Base else return std::nullopt; // Base | Unrelated ~ Base | Unrelated } else if (auto [nt, it] = get2(left, right); nt && it) { std::vector parts; for (TypeId option : it) { if (auto result = unionType(left, option)) parts.push_back(*result); else { // TODO: does there exist a reduced form such that `~T | A` hasn't already reduced it, if `A & B` is irreducible? // I want to say yes, but I can't generate a case that hits this code path. parts.push_back(arena->addType(UnionType{{left, option}})); } } return foldl(begin(parts), end(parts), &TypeReducer::intersectionType); // ~T | (A & B) ~ (~T | A) & (~T | B) } else if (auto [it, nt] = get2(left, right); it && nt) return unionType(right, left); // (A & B) | ~T ~ ~T | (A & B) else if (auto [nl, nr] = get2(left, right); nl && nr) { // These should've been reduced already. TypeId nlTy = follow(nl->ty); TypeId nrTy = follow(nr->ty); LUAU_ASSERT(!get(nlTy) && !get(nrTy)); LUAU_ASSERT(!get(nlTy) && !get(nrTy)); LUAU_ASSERT(!get(nlTy) && !get(nrTy)); LUAU_ASSERT(!get(nlTy) && !get(nrTy)); LUAU_ASSERT(!get(nlTy) && !get(nrTy)); if (auto [npl, npr] = get2(nlTy, nrTy); npl && npr) { if (npl->type == npr->type) return left; // ~P1 | ~P2 ~ ~P1 iff P1 == P2 else return builtinTypes->unknownType; // ~P1 | ~P2 ~ ~P1 iff P1 != P2 } else if (auto [nsl, nsr] = get2(nlTy, nrTy); nsl && nsr) { if (*nsl == *nsr) return left; // ~"A" | ~"A" ~ ~"A" else return builtinTypes->unknownType; // ~"A" | ~"B" ~ unknown } else if (auto [ns, np] = get2(nlTy, nrTy); ns && np) { if (get(ns) && np->type == PrimitiveType::String) return left; // ~"A" | ~string ~ ~"A" else if (get(ns) && np->type == PrimitiveType::Boolean) return left; // ~false | ~boolean ~ ~false else return builtinTypes->unknownType; // ~"A" | ~P ~ unknown } else if (auto [np, ns] = get2(nlTy, nrTy); np && ns) return unionType(right, left); // ~P | ~S ~ ~S | ~P else return std::nullopt; // TODO! } else if (auto nl = get(left)) { // These should've been reduced already. TypeId nlTy = follow(nl->ty); LUAU_ASSERT(!get(nlTy)); LUAU_ASSERT(!get(nlTy)); LUAU_ASSERT(!get(nlTy)); LUAU_ASSERT(!get(nlTy)); LUAU_ASSERT(!get(nlTy)); if (auto [np, p] = get2(nlTy, right); np && p) { if (np->type == p->type) return builtinTypes->unknownType; // ~P1 | P2 ~ unknown iff P1 == P2 else return left; // ~P1 | P2 ~ ~P1 iff P1 != P2 } else if (auto [ns, s] = get2(nlTy, right); ns && s) { if (*ns == *s) return builtinTypes->unknownType; // ~"A" | "A" ~ unknown else return left; // ~"A" | "B" ~ ~"A" } else if (auto [ns, p] = get2(nlTy, right); ns && p) { if (get(ns) && p->type == PrimitiveType::String) return builtinTypes->unknownType; // ~"A" | string ~ unknown else if (get(ns) && p->type == PrimitiveType::Boolean) return builtinTypes->unknownType; // ~false | boolean ~ unknown else return left; // ~"A" | T ~ ~"A" } else if (auto [np, s] = get2(nlTy, right); np && s) { if (np->type == PrimitiveType::String && get(s)) return std::nullopt; // ~string | "A" ~ ~string | "A" else if (np->type == PrimitiveType::Boolean && get(s)) { const BooleanSingleton* b = get(s); return negationType(arena->addType(SingletonType{BooleanSingleton{!b->value}})); // ~boolean | false ~ ~true } else return left; // ~P | "A" ~ ~P } else if (auto [nc, c] = get2(nlTy, right); nc && c) { if (isSubclass(nc, c)) return builtinTypes->unknownType; // ~Derived | Base ~ unknown else if (isSubclass(c, nc)) return std::nullopt; // ~Base | Derived ~ ~Base | Derived else return left; // ~Base | Unrelated ~ ~Base } else return std::nullopt; // TODO } else if (get(right)) return unionType(right, left); // T | ~U ~ ~U | T else return std::nullopt; // for all T and U except the ones handled above, T | U ~ T | U } TypeId TypeReducer::tableType(TypeId ty) { RecursionGuard rg = guard(ty); if (auto mt = get(ty)) { auto [copiedTy, copied] = copy(ty, mt); copied->table = reduce(mt->table); copied->metatable = reduce(mt->metatable); return copiedTy; } else if (auto tt = get(ty)) { auto [copiedTy, copied] = copy(ty, tt); for (auto& [name, prop] : copied->props) prop.type = reduce(prop.type); if (auto& indexer = copied->indexer) { indexer->indexType = reduce(indexer->indexType); indexer->indexResultType = reduce(indexer->indexResultType); } for (TypeId& ty : copied->instantiatedTypeParams) ty = reduce(ty); for (TypePackId& tp : copied->instantiatedTypePackParams) tp = reduce(tp); return copiedTy; } else handle->ice("Unexpected type in TypeReducer::tableType"); } TypeId TypeReducer::functionType(TypeId ty) { RecursionGuard rg = guard(ty); const FunctionType* f = get(ty); if (!f) handle->ice("TypeReducer::reduce expects a FunctionType"); // TODO: once we have bounded quantification, we need to be able to reduce the generic bounds. auto [copiedTy, copied] = copy(ty, f); copied->argTypes = reduce(f->argTypes); copied->retTypes = reduce(f->retTypes); return copiedTy; } TypeId TypeReducer::negationType(TypeId ty) { RecursionGuard rg = guard(ty); if (auto nn = get(ty)) return nn->ty; // ~~T ~ T else if (get(ty)) return builtinTypes->unknownType; // ~never ~ unknown else if (get(ty)) return builtinTypes->neverType; // ~unknown ~ never else if (get(ty)) return builtinTypes->anyType; // ~any ~ any else if (auto ni = get(ty)) { std::vector options; for (TypeId part : ni) options.push_back(negationType(part)); return foldl(begin(options), end(options), &TypeReducer::unionType); // ~(T & U) ~ (~T | ~U) } else if (auto nu = get(ty)) { std::vector parts; for (TypeId option : nu) parts.push_back(negationType(option)); return foldl(begin(parts), end(parts), &TypeReducer::intersectionType); // ~(T | U) ~ (~T & ~U) } else return arena->addType(NegationType{ty}); // for all T except the ones handled above, ~T ~ ~T } RecursionGuard TypeReducer::guard(TypeId ty) { seen.push_back(ty); return RecursionGuard{&depth, FInt::LuauTypeReductionRecursionLimit, &seen}; } RecursionGuard TypeReducer::guard(TypePackId tp) { seen.push_back(tp); return RecursionGuard{&depth, FInt::LuauTypeReductionRecursionLimit, &seen}; } void TypeReducer::checkCacheable(TypeId ty) { if (!cacheOk) return; ty = follow(ty); // Only does shallow check, the TypeReducer itself already does deep traversal. if (get(ty) || get(ty) || get(ty)) cacheOk = false; else if (auto tt = get(ty); tt && (tt->state == TableState::Free || tt->state == TableState::Unsealed)) cacheOk = false; } void TypeReducer::checkCacheable(TypePackId tp) { if (!cacheOk) return; tp = follow(tp); // Only does shallow check, the TypeReducer itself already does deep traversal. if (get(tp) || get(tp)) cacheOk = false; } } // namespace TypeReduction::TypeReduction(NotNull arena, NotNull builtinTypes, NotNull handle) : arena(arena) , builtinTypes(builtinTypes) , handle(handle) { } std::optional TypeReduction::reduce(TypeId ty) { if (auto found = cachedTypes.find(ty)) return *found; auto [reducedTy, cacheOk] = reduceImpl(ty); if (cacheOk) cachedTypes[ty] = *reducedTy; return reducedTy; } std::optional TypeReduction::reduce(TypePackId tp) { if (auto found = cachedTypePacks.find(tp)) return *found; auto [reducedTp, cacheOk] = reduceImpl(tp); if (cacheOk) cachedTypePacks[tp] = *reducedTp; return reducedTp; } std::pair, bool> TypeReduction::reduceImpl(TypeId ty) { if (FFlag::DebugLuauDontReduceTypes) return {ty, false}; if (hasExceededCartesianProductLimit(ty)) return {std::nullopt, false}; try { TypeReducer reducer{arena, builtinTypes, handle}; return {reducer.reduce(ty), reducer.cacheOk}; } catch (const RecursionLimitException&) { return {std::nullopt, false}; } } std::pair, bool> TypeReduction::reduceImpl(TypePackId tp) { if (FFlag::DebugLuauDontReduceTypes) return {tp, false}; if (hasExceededCartesianProductLimit(tp)) return {std::nullopt, false}; try { TypeReducer reducer{arena, builtinTypes, handle}; return {reducer.reduce(tp), reducer.cacheOk}; } catch (const RecursionLimitException&) { return {std::nullopt, false}; } } std::optional TypeReduction::reduce(const TypeFun& fun) { if (FFlag::DebugLuauDontReduceTypes) return fun; // TODO: once we have bounded quantification, we need to be able to reduce the generic bounds. if (auto reducedTy = reduce(fun.type)) return TypeFun{fun.typeParams, fun.typePackParams, *reducedTy}; return std::nullopt; } size_t TypeReduction::cartesianProductSize(TypeId ty) const { ty = follow(ty); auto it = get(follow(ty)); if (!it) return 1; return std::accumulate(begin(it), end(it), size_t(1), [](size_t acc, TypeId ty) { if (auto ut = get(ty)) return acc * std::distance(begin(ut), end(ut)); else if (get(ty)) return acc * 0; else return acc * 1; }); } bool TypeReduction::hasExceededCartesianProductLimit(TypeId ty) const { return cartesianProductSize(ty) >= size_t(FInt::LuauTypeReductionCartesianProductLimit); } bool TypeReduction::hasExceededCartesianProductLimit(TypePackId tp) const { TypePackIterator it = begin(tp); while (it != end(tp)) { if (hasExceededCartesianProductLimit(*it)) return true; ++it; } if (auto tail = it.tail()) { if (auto vtp = get(follow(*tail))) { if (hasExceededCartesianProductLimit(vtp->ty)) return true; } } return false; } } // namespace Luau