// This file is part of the Luau programming language and is licensed under MIT License; see LICENSE.txt for details #include "Luau/Simplify.h" #include "Luau/Normalize.h" // TypeIds #include "Luau/RecursionCounter.h" #include "Luau/ToString.h" #include "Luau/TypeArena.h" #include "Luau/TypeUtils.h" #include LUAU_FASTINT(LuauTypeReductionRecursionLimit) LUAU_FASTFLAG(DebugLuauDeferredConstraintResolution) namespace Luau { struct TypeSimplifier { NotNull builtinTypes; NotNull arena; std::set blockedTypes; int recursionDepth = 0; TypeId mkNegation(TypeId ty); TypeId intersectFromParts(std::set parts); TypeId intersectUnionWithType(TypeId unionTy, TypeId right); TypeId intersectUnions(TypeId left, TypeId right); TypeId intersectNegatedUnion(TypeId unionTy, TypeId right); TypeId intersectTypeWithNegation(TypeId a, TypeId b); TypeId intersectNegations(TypeId a, TypeId b); TypeId intersectIntersectionWithType(TypeId left, TypeId right); // Attempt to intersect the two types. Does not recurse. Does not handle // unions, intersections, or negations. std::optional basicIntersect(TypeId left, TypeId right); TypeId intersect(TypeId ty, TypeId discriminant); TypeId union_(TypeId ty, TypeId discriminant); TypeId simplify(TypeId ty); TypeId simplify(TypeId ty, DenseHashSet& seen); }; // Match the exact type false|nil static bool isFalsyType(TypeId ty) { ty = follow(ty); const UnionType* ut = get(ty); if (!ut) return false; bool hasFalse = false; bool hasNil = false; auto it = begin(ut); if (it == end(ut)) return false; TypeId t = follow(*it); if (auto pt = get(t); pt && pt->type == PrimitiveType::NilType) hasNil = true; else if (auto st = get(t); st && st->variant == BooleanSingleton{false}) hasFalse = true; else return false; ++it; if (it == end(ut)) return false; t = follow(*it); if (auto pt = get(t); pt && pt->type == PrimitiveType::NilType) hasNil = true; else if (auto st = get(t); st && st->variant == BooleanSingleton{false}) hasFalse = true; else return false; ++it; if (it != end(ut)) return false; return hasFalse && hasNil; } // Match the exact type ~(false|nil) bool isTruthyType(TypeId ty) { ty = follow(ty); const NegationType* nt = get(ty); if (!nt) return false; return isFalsyType(nt->ty); } Relation flip(Relation rel) { switch (rel) { case Relation::Subset: return Relation::Superset; case Relation::Superset: return Relation::Subset; default: return rel; } } // FIXME: I'm not completely certain that this function is theoretically reasonable. Relation combine(Relation a, Relation b) { switch (a) { case Relation::Disjoint: switch (b) { case Relation::Disjoint: return Relation::Disjoint; case Relation::Coincident: return Relation::Superset; case Relation::Intersects: return Relation::Intersects; case Relation::Subset: return Relation::Intersects; case Relation::Superset: return Relation::Intersects; } case Relation::Coincident: switch (b) { case Relation::Disjoint: return Relation::Coincident; case Relation::Coincident: return Relation::Coincident; case Relation::Intersects: return Relation::Superset; case Relation::Subset: return Relation::Coincident; case Relation::Superset: return Relation::Intersects; } case Relation::Superset: switch (b) { case Relation::Disjoint: return Relation::Superset; case Relation::Coincident: return Relation::Superset; case Relation::Intersects: return Relation::Intersects; case Relation::Subset: return Relation::Intersects; case Relation::Superset: return Relation::Superset; } case Relation::Subset: switch (b) { case Relation::Disjoint: return Relation::Subset; case Relation::Coincident: return Relation::Coincident; case Relation::Intersects: return Relation::Intersects; case Relation::Subset: return Relation::Subset; case Relation::Superset: return Relation::Intersects; } case Relation::Intersects: switch (b) { case Relation::Disjoint: return Relation::Intersects; case Relation::Coincident: return Relation::Superset; case Relation::Intersects: return Relation::Intersects; case Relation::Subset: return Relation::Intersects; case Relation::Superset: return Relation::Intersects; } } LUAU_UNREACHABLE(); return Relation::Intersects; } // Given A & B, what is A & ~B? Relation invert(Relation r) { switch (r) { case Relation::Disjoint: return Relation::Subset; case Relation::Coincident: return Relation::Disjoint; case Relation::Intersects: return Relation::Intersects; case Relation::Subset: return Relation::Disjoint; case Relation::Superset: return Relation::Intersects; } LUAU_UNREACHABLE(); return Relation::Intersects; } static bool isTypeVariable(TypeId ty) { return get(ty) || get(ty) || get(ty) || get(ty); } Relation relate(TypeId left, TypeId right); Relation relateTables(TypeId left, TypeId right) { NotNull leftTable{get(left)}; NotNull rightTable{get(right)}; LUAU_ASSERT(1 == rightTable->props.size()); // Disjoint props have nothing in common // t1 with props p1's cannot appear in t2 and t2 with props p2's cannot appear in t1 bool foundPropFromLeftInRight = std::any_of(begin(leftTable->props), end(leftTable->props), [&](auto prop) { return rightTable->props.find(prop.first) != end(rightTable->props); }); bool foundPropFromRightInLeft = std::any_of(begin(rightTable->props), end(rightTable->props), [&](auto prop) { return leftTable->props.find(prop.first) != end(leftTable->props); }); if (!(foundPropFromLeftInRight || foundPropFromRightInLeft) && leftTable->props.size() >= 1 && rightTable->props.size() >= 1) return Relation::Disjoint; const auto [propName, rightProp] = *begin(rightTable->props); auto it = leftTable->props.find(propName); if (it == leftTable->props.end()) { // Every table lacking a property is a supertype of a table having that // property but the reverse is not true. return Relation::Superset; } const Property leftProp = it->second; Relation r = relate(leftProp.type(), rightProp.type()); if (r == Relation::Coincident && 1 != leftTable->props.size()) { // eg {tag: "cat", prop: string} & {tag: "cat"} return Relation::Subset; } else return r; } // A cheap and approximate subtype test Relation relate(TypeId left, TypeId right) { // TODO nice to have: Relate functions of equal argument and return arity left = follow(left); right = follow(right); if (left == right) return Relation::Coincident; if (get(left)) { if (get(right)) return Relation::Subset; else if (get(right)) return Relation::Coincident; else if (get(right)) return Relation::Disjoint; else return Relation::Superset; } if (get(right)) return flip(relate(right, left)); if (get(left)) { if (get(right)) return Relation::Coincident; else return Relation::Superset; } if (get(right)) return flip(relate(right, left)); // Type variables // * FreeType // * GenericType // * BlockedType // * PendingExpansionType // Tops and bottoms // * ErrorType // * AnyType // * NeverType // * UnknownType // Concrete // * PrimitiveType // * SingletonType // * FunctionType // * TableType // * MetatableType // * ClassType // * UnionType // * IntersectionType // * NegationType if (isTypeVariable(left) || isTypeVariable(right)) return Relation::Intersects; if (get(left)) { if (get(right)) return Relation::Coincident; else if (get(right)) return Relation::Subset; else return Relation::Disjoint; } if (get(right)) return flip(relate(right, left)); if (get(left)) { if (get(right)) return Relation::Coincident; else return Relation::Subset; } if (get(right)) return flip(relate(right, left)); if (auto ut = get(left)) return Relation::Intersects; else if (auto ut = get(right)) return Relation::Intersects; if (auto ut = get(left)) return Relation::Intersects; else if (auto ut = get(right)) { std::vector opts; for (TypeId part : ut) if (relate(left, part) == Relation::Subset) return Relation::Subset; return Relation::Intersects; } if (auto rnt = get(right)) { Relation a = relate(left, rnt->ty); switch (a) { case Relation::Coincident: // number & ~number return Relation::Disjoint; case Relation::Disjoint: if (get(left)) { // ~number & ~string return Relation::Intersects; } else { // number & ~string return Relation::Subset; } case Relation::Intersects: // ~(false?) & ~boolean return Relation::Intersects; case Relation::Subset: // "hello" & ~string return Relation::Disjoint; case Relation::Superset: // ~function & ~(false?) -> ~function // boolean & ~(false?) -> true // string & ~"hello" -> string & ~"hello" return Relation::Intersects; } } else if (get(left)) return flip(relate(right, left)); if (auto lp = get(left)) { if (auto rp = get(right)) { if (lp->type == rp->type) return Relation::Coincident; else return Relation::Disjoint; } if (auto rs = get(right)) { if (lp->type == PrimitiveType::String && rs->variant.get_if()) return Relation::Superset; else if (lp->type == PrimitiveType::Boolean && rs->variant.get_if()) return Relation::Superset; else return Relation::Disjoint; } if (lp->type == PrimitiveType::Function) { if (get(right)) return Relation::Superset; else return Relation::Disjoint; } if (lp->type == PrimitiveType::Table) { if (get(right)) return Relation::Superset; else return Relation::Disjoint; } if (get(right) || get(right) || get(right) || get(right)) return Relation::Disjoint; } if (auto ls = get(left)) { if (get(right) || get(right) || get(right) || get(right)) return Relation::Disjoint; if (get(right)) return flip(relate(right, left)); if (auto rs = get(right)) { if (ls->variant == rs->variant) return Relation::Coincident; else return Relation::Disjoint; } } if (get(left)) { if (auto rp = get(right)) { if (rp->type == PrimitiveType::Function) return Relation::Subset; else return Relation::Disjoint; } else return Relation::Intersects; } if (auto lt = get(left)) { if (auto rp = get(right)) { if (rp->type == PrimitiveType::Table) return Relation::Subset; else return Relation::Disjoint; } else if (auto rt = get(right)) { // TODO PROBABLY indexers and metatables. if (1 == rt->props.size()) { Relation r = relateTables(left, right); /* * A reduction of these intersections is certainly possible, but * it would require minting new table types. Also, I don't think * it's super likely for this to arise from a refinement. * * Time will tell! * * ex we simplify this * {tag: string} & {tag: "cat"} * but not this * {tag: string, prop: number} & {tag: "cat"} */ if (lt->props.size() > 1 && r == Relation::Superset) return Relation::Intersects; else return r; } else if (1 == lt->props.size()) return flip(relate(right, left)); else return Relation::Intersects; } // TODO metatables return Relation::Disjoint; } if (auto ct = get(left)) { if (auto rct = get(right)) { if (isSubclass(ct, rct)) return Relation::Subset; else if (isSubclass(rct, ct)) return Relation::Superset; else return Relation::Disjoint; } return Relation::Disjoint; } return Relation::Intersects; } TypeId TypeSimplifier::mkNegation(TypeId ty) { TypeId result = nullptr; if (ty == builtinTypes->truthyType) result = builtinTypes->falsyType; else if (ty == builtinTypes->falsyType) result = builtinTypes->truthyType; else if (auto ntv = get(ty)) result = follow(ntv->ty); else result = arena->addType(NegationType{ty}); return result; } TypeId TypeSimplifier::intersectFromParts(std::set parts) { if (0 == parts.size()) return builtinTypes->neverType; else if (1 == parts.size()) return *begin(parts); { auto it = begin(parts); while (it != end(parts)) { TypeId t = follow(*it); auto copy = it; ++it; if (auto ut = get(t)) { for (TypeId part : ut) parts.insert(part); parts.erase(copy); } } } std::set newParts; /* * It is possible that the parts of the passed intersection are themselves * reducable. * * eg false & boolean * * We do a comparison between each pair of types and look for things that we * can elide. */ for (TypeId part : parts) { if (newParts.empty()) { newParts.insert(part); continue; } auto it = begin(newParts); while (it != end(newParts)) { TypeId p = *it; switch (relate(part, p)) { case Relation::Disjoint: // eg boolean & string return builtinTypes->neverType; case Relation::Subset: { /* part is a subset of p. Remove p from the set and replace it * with part. * * eg boolean & true */ auto saveIt = it; ++it; newParts.erase(saveIt); continue; } case Relation::Coincident: case Relation::Superset: { /* part is coincident or a superset of p. We do not need to * include part in the final intersection. * * ex true & boolean */ ++it; continue; } case Relation::Intersects: { /* It's complicated! A simplification may still be possible, * but we have to pull the types apart to figure it out. * * ex boolean & ~false */ std::optional simplified = basicIntersect(part, p); auto saveIt = it; ++it; if (simplified) { newParts.erase(saveIt); newParts.insert(*simplified); } else newParts.insert(part); continue; } } } } if (0 == newParts.size()) return builtinTypes->neverType; else if (1 == newParts.size()) return *begin(newParts); else return arena->addType(IntersectionType{std::vector{begin(newParts), end(newParts)}}); } TypeId TypeSimplifier::intersectUnionWithType(TypeId left, TypeId right) { const UnionType* leftUnion = get(left); LUAU_ASSERT(leftUnion); bool changed = false; std::set newParts; for (TypeId part : leftUnion) { TypeId simplified = intersect(right, part); changed |= simplified != part; if (get(simplified)) { changed = true; continue; } newParts.insert(simplified); } if (!changed) return left; else if (newParts.empty()) return builtinTypes->neverType; else if (newParts.size() == 1) return *begin(newParts); else return arena->addType(UnionType{std::vector(begin(newParts), end(newParts))}); } TypeId TypeSimplifier::intersectUnions(TypeId left, TypeId right) { const UnionType* leftUnion = get(left); LUAU_ASSERT(leftUnion); const UnionType* rightUnion = get(right); LUAU_ASSERT(rightUnion); std::set newParts; for (TypeId leftPart : leftUnion) { for (TypeId rightPart : rightUnion) { TypeId simplified = intersect(leftPart, rightPart); if (get(simplified)) continue; newParts.insert(simplified); } } if (newParts.empty()) return builtinTypes->neverType; else if (newParts.size() == 1) return *begin(newParts); else return arena->addType(UnionType{std::vector(begin(newParts), end(newParts))}); } TypeId TypeSimplifier::intersectNegatedUnion(TypeId left, TypeId right) { // ~(A | B) & C // (~A & C) & (~B & C) const NegationType* leftNegation = get(left); LUAU_ASSERT(leftNegation); TypeId negatedTy = follow(leftNegation->ty); const UnionType* negatedUnion = get(negatedTy); LUAU_ASSERT(negatedUnion); bool changed = false; std::set newParts; for (TypeId part : negatedUnion) { Relation r = relate(part, right); switch (r) { case Relation::Disjoint: // If A is disjoint from B, then ~A & B is just B. // // ~(false?) & true // (~false & true) & (~nil & true) // true & true newParts.insert(right); break; case Relation::Coincident: // If A is coincident with or a superset of B, then ~A & B is never. // // ~(false?) & false // (~false & false) & (~nil & false) // never & false // // fallthrough case Relation::Superset: // If A is a superset of B, then ~A & B is never. // // ~(boolean | nil) & true // (~boolean & true) & (~boolean & nil) // never & nil return builtinTypes->neverType; case Relation::Subset: case Relation::Intersects: // If A is a subset of B, then ~A & B is a bit more complicated. We need to think harder. // // ~(false?) & boolean // (~false & boolean) & (~nil & boolean) // true & boolean TypeId simplified = intersectTypeWithNegation(mkNegation(part), right); changed |= simplified != right; if (get(simplified)) changed = true; else newParts.insert(simplified); break; } } if (!changed) return right; else return intersectFromParts(std::move(newParts)); } TypeId TypeSimplifier::intersectTypeWithNegation(TypeId left, TypeId right) { const NegationType* leftNegation = get(left); LUAU_ASSERT(leftNegation); TypeId negatedTy = follow(leftNegation->ty); if (negatedTy == right) return builtinTypes->neverType; if (auto ut = get(negatedTy)) { // ~(A | B) & C // (~A & C) & (~B & C) bool changed = false; std::set newParts; for (TypeId part : ut) { Relation r = relate(part, right); switch (r) { case Relation::Coincident: // ~(false?) & nil // (~false & nil) & (~nil & nil) // nil & never // // fallthrough case Relation::Superset: // ~(boolean | string) & true // (~boolean & true) & (~boolean & string) // never & string return builtinTypes->neverType; case Relation::Disjoint: // ~nil & boolean newParts.insert(right); break; case Relation::Subset: // ~false & boolean // fallthrough case Relation::Intersects: // FIXME: The mkNegation here is pretty unfortunate. // Memoizing this will probably be important. changed = true; newParts.insert(right); newParts.insert(mkNegation(part)); } } if (!changed) return right; else return intersectFromParts(std::move(newParts)); } if (auto rightUnion = get(right)) { // ~A & (B | C) bool changed = false; std::set newParts; for (TypeId part : rightUnion) { Relation r = relate(negatedTy, part); switch (r) { case Relation::Coincident: changed = true; continue; case Relation::Disjoint: newParts.insert(part); break; case Relation::Superset: changed = true; continue; case Relation::Subset: // fallthrough case Relation::Intersects: changed = true; newParts.insert(arena->addType(IntersectionType{{left, part}})); } } if (!changed) return right; else if (0 == newParts.size()) return builtinTypes->neverType; else if (1 == newParts.size()) return *begin(newParts); else return arena->addType(UnionType{std::vector{begin(newParts), end(newParts)}}); } if (auto pt = get(right); pt && pt->type == PrimitiveType::Boolean) { if (auto st = get(negatedTy)) { if (st->variant == BooleanSingleton{true}) return builtinTypes->falseType; else if (st->variant == BooleanSingleton{false}) return builtinTypes->trueType; else // boolean & ~"hello" return builtinTypes->booleanType; } } Relation r = relate(negatedTy, right); switch (r) { case Relation::Disjoint: // ~boolean & string return right; case Relation::Coincident: // ~string & string // fallthrough case Relation::Superset: // ~string & "hello" return builtinTypes->neverType; case Relation::Subset: // ~string & unknown // ~"hello" & string // fallthrough case Relation::Intersects: // ~("hello" | boolean) & string // fallthrough default: return arena->addType(IntersectionType{{left, right}}); } } TypeId TypeSimplifier::intersectNegations(TypeId left, TypeId right) { const NegationType* leftNegation = get(left); LUAU_ASSERT(leftNegation); if (get(follow(leftNegation->ty))) return intersectNegatedUnion(left, right); const NegationType* rightNegation = get(right); LUAU_ASSERT(rightNegation); if (get(follow(rightNegation->ty))) return intersectNegatedUnion(right, left); Relation r = relate(leftNegation->ty, rightNegation->ty); switch (r) { case Relation::Coincident: // ~true & ~true return left; case Relation::Subset: // ~true & ~boolean return right; case Relation::Superset: // ~boolean & ~true return left; case Relation::Intersects: case Relation::Disjoint: default: // ~boolean & ~string return arena->addType(IntersectionType{{left, right}}); } } TypeId TypeSimplifier::intersectIntersectionWithType(TypeId left, TypeId right) { const IntersectionType* leftIntersection = get(left); LUAU_ASSERT(leftIntersection); bool changed = false; std::set newParts; for (TypeId part : leftIntersection) { Relation r = relate(part, right); switch (r) { case Relation::Disjoint: return builtinTypes->neverType; case Relation::Coincident: newParts.insert(part); continue; case Relation::Subset: newParts.insert(part); continue; case Relation::Superset: newParts.insert(right); changed = true; continue; default: newParts.insert(part); newParts.insert(right); changed = true; continue; } } // It is sometimes the case that an intersection operation will result in // clipping a free type from the result. // // eg (number & 'a) & string --> never // // We want to only report the free types that are part of the result. for (TypeId part : newParts) { if (isTypeVariable(part)) blockedTypes.insert(part); } if (!changed) return left; return intersectFromParts(std::move(newParts)); } std::optional TypeSimplifier::basicIntersect(TypeId left, TypeId right) { if (get(left)) return right; if (get(right)) return left; if (get(left)) return left; if (get(right)) return right; if (auto pt = get(left); pt && pt->type == PrimitiveType::Boolean) { if (auto st = get(right); st && st->variant.get_if()) return right; if (auto nt = get(right)) { if (auto st = get(follow(nt->ty)); st && st->variant.get_if()) { if (st->variant == BooleanSingleton{true}) return builtinTypes->falseType; else return builtinTypes->trueType; } } } else if (auto pt = get(right); pt && pt->type == PrimitiveType::Boolean) { if (auto st = get(left); st && st->variant.get_if()) return left; if (auto nt = get(left)) { if (auto st = get(follow(nt->ty)); st && st->variant.get_if()) { if (st->variant == BooleanSingleton{true}) return builtinTypes->falseType; else return builtinTypes->trueType; } } } if (const auto [lt, rt] = get2(left, right); lt && rt) { if (1 == lt->props.size()) { const auto [propName, leftProp] = *begin(lt->props); auto it = rt->props.find(propName); if (it != rt->props.end()) { Relation r = relate(leftProp.type(), it->second.type()); switch (r) { case Relation::Disjoint: return builtinTypes->neverType; case Relation::Coincident: return right; default: break; } } } else if (1 == rt->props.size()) return basicIntersect(right, left); } Relation relation = relate(left, right); if (left == right || Relation::Coincident == relation) return left; if (relation == Relation::Disjoint) return builtinTypes->neverType; else if (relation == Relation::Subset) return left; else if (relation == Relation::Superset) return right; return std::nullopt; } TypeId TypeSimplifier::intersect(TypeId left, TypeId right) { RecursionLimiter rl(&recursionDepth, 15); left = simplify(left); right = simplify(right); if (get(left)) return right; if (get(right)) return left; if (get(left)) return left; if (get(right)) return right; if (auto lf = get(left)) { Relation r = relate(lf->upperBound, right); if (r == Relation::Subset || r == Relation::Coincident) return left; } else if (auto rf = get(right)) { Relation r = relate(left, rf->upperBound); if (r == Relation::Superset || r == Relation::Coincident) return right; } if (isTypeVariable(left)) { blockedTypes.insert(left); return arena->addType(IntersectionType{{left, right}}); } if (isTypeVariable(right)) { blockedTypes.insert(right); return arena->addType(IntersectionType{{left, right}}); } if (auto ut = get(left)) { if (get(right)) return intersectUnions(left, right); else return intersectUnionWithType(left, right); } else if (auto ut = get(right)) return intersectUnionWithType(right, left); if (auto it = get(left)) return intersectIntersectionWithType(left, right); else if (auto it = get(right)) return intersectIntersectionWithType(right, left); if (get(left)) { if (get(right)) return intersectNegations(left, right); else return intersectTypeWithNegation(left, right); } else if (get(right)) return intersectTypeWithNegation(right, left); std::optional res = basicIntersect(left, right); if (res) return *res; else return arena->addType(IntersectionType{{left, right}}); } TypeId TypeSimplifier::union_(TypeId left, TypeId right) { RecursionLimiter rl(&recursionDepth, 15); left = simplify(left); right = simplify(right); if (get(left)) return right; if (get(right)) return left; if (auto leftUnion = get(left)) { bool changed = false; std::set newParts; for (TypeId part : leftUnion) { if (get(part)) { changed = true; continue; } Relation r = relate(part, right); switch (r) { case Relation::Coincident: case Relation::Superset: return left; default: newParts.insert(part); newParts.insert(right); changed = true; break; } } if (!changed) return left; if (1 == newParts.size()) return *begin(newParts); return arena->addType(UnionType{std::vector{begin(newParts), end(newParts)}}); } else if (get(right)) return union_(right, left); Relation r = relate(left, right); if (left == right || r == Relation::Coincident || r == Relation::Superset) return left; if (r == Relation::Subset) return right; if (auto as = get(left)) { if (auto abs = as->variant.get_if()) { if (auto bs = get(right)) { if (auto bbs = bs->variant.get_if()) { if (abs->value != bbs->value) return builtinTypes->booleanType; } } } } return arena->addType(UnionType{{left, right}}); } TypeId TypeSimplifier::simplify(TypeId ty) { DenseHashSet seen{nullptr}; return simplify(ty, seen); } TypeId TypeSimplifier::simplify(TypeId ty, DenseHashSet& seen) { RecursionLimiter limiter(&recursionDepth, 60); ty = follow(ty); if (seen.find(ty)) return ty; seen.insert(ty); if (auto nt = get(ty)) { TypeId negatedTy = follow(nt->ty); if (get(negatedTy)) return builtinTypes->neverType; else if (get(negatedTy)) return builtinTypes->anyType; if (auto nnt = get(negatedTy)) return simplify(nnt->ty, seen); } // Promote {x: never} to never if (auto tt = get(ty)) { if (1 == tt->props.size()) { TypeId propTy = simplify(begin(tt->props)->second.type(), seen); if (get(propTy)) return builtinTypes->neverType; } } return ty; } SimplifyResult simplifyIntersection(NotNull builtinTypes, NotNull arena, TypeId left, TypeId right) { LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution); TypeSimplifier s{builtinTypes, arena}; // fprintf(stderr, "Intersect %s and %s ...\n", toString(left).c_str(), toString(right).c_str()); TypeId res = s.intersect(left, right); // fprintf(stderr, "Intersect %s and %s -> %s\n", toString(left).c_str(), toString(right).c_str(), toString(res).c_str()); return SimplifyResult{res, std::move(s.blockedTypes)}; } SimplifyResult simplifyUnion(NotNull builtinTypes, NotNull arena, TypeId left, TypeId right) { LUAU_ASSERT(FFlag::DebugLuauDeferredConstraintResolution); TypeSimplifier s{builtinTypes, arena}; TypeId res = s.union_(left, right); // fprintf(stderr, "Union %s and %s -> %s\n", toString(left).c_str(), toString(right).c_str(), toString(res).c_str()); return SimplifyResult{res, std::move(s.blockedTypes)}; } } // namespace Luau