luau/Analysis/src/Simplify.cpp
menarulalam 8fe64db609
Sync to upstream/release/679 (#1884)
# What's Changed?

We've been hard at work fixing bugs and introducing new features!

## VM 
* Include constant-folding information in Luau cost model for inlining
and loop unrolling
   * ~1% improvement in compile times

## New Type Solver
* `Luau::shallowClone`'s last argument, whether to clone persistent
(builtin) types, is now non-optional.
* Refinements on properties of tables are now computed with a `read`
table property. This resolves some issues around refining table
properies and then trying to set them. Fixes #1344. Fixes #1651.
```
if foo.bar then
    -- Prior to this release, this would be `typeof(foo) & { bar: ~(false?) }
    -- Now, this is `typeof(foo) & { read bar: ~(false?) }
end
```
* The type function `keyof` should respect the empty string as a
property, as in:
```
-- equivalent to type Foo =""
type Foo = keyof<{ [""]: number }>
```
* Descend into literals to report subtyping errors for function calls:
this both improves bidirectional inference and makes errors more
specific. Before, the error reporting for a table with incorrect members
passed to a function would cite the entire table, but now it only cites
the members that are incorrectly typed.
* Fixes a case where intersecting two tables without any common
properties would create `never`, instead of a table with both of their
properties.

# Internal Contributors
Co-authored-by: Ariel Weiss <aaronweiss@roblox.com>
Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: James McNellis <jmcnellis@roblox.com>
Co-authored-by: Sora Kanosue <skanosue@roblox.com>
Co-authored-by: Talha Pathan <tpathan@roblox.com>
Co-authored-by: Vighnesh Vijay <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>

---------

Co-authored-by: Hunter Goldstein <hgoldstein@roblox.com>
Co-authored-by: Varun Saini <61795485+vrn-sn@users.noreply.github.com>
Co-authored-by: Alexander Youngblood <ayoungblood@roblox.com>
Co-authored-by: Aviral Goel <agoel@roblox.com>
Co-authored-by: Vighnesh <vvijay@roblox.com>
Co-authored-by: Vyacheslav Egorov <vegorov@roblox.com>
Co-authored-by: Ariel Weiss <aaronweiss@roblox.com>
Co-authored-by: Andy Friesen <afriesen@roblox.com>
2025-06-20 15:55:42 -07:00

1961 lines
58 KiB
C++

// 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/Clone.h"
#include "Luau/Common.h"
#include "Luau/DenseHash.h"
#include "Luau/RecursionCounter.h"
#include "Luau/Set.h"
#include "Luau/Type.h"
#include "Luau/TypeArena.h"
#include "Luau/TypeIds.h"
#include "Luau/TypePairHash.h"
#include "Luau/TypeUtils.h"
#include <algorithm>
LUAU_FASTINT(LuauTypeReductionRecursionLimit)
LUAU_FASTFLAG(LuauSolverV2)
LUAU_DYNAMIC_FASTINTVARIABLE(LuauSimplificationComplexityLimit, 8)
LUAU_FASTFLAGVARIABLE(LuauSimplificationTableExternType)
LUAU_FASTFLAG(LuauRemoveTypeCallsForReadWriteProps)
LUAU_FASTFLAGVARIABLE(LuauRelateTablesAreNeverDisjoint)
LUAU_FASTFLAG(LuauRefineTablesWithReadType)
namespace Luau
{
using SimplifierSeenSet = Set<std::pair<TypeId, TypeId>, TypePairHash>;
struct TypeSimplifier
{
NotNull<BuiltinTypes> builtinTypes;
NotNull<TypeArena> arena;
DenseHashSet<TypeId> blockedTypes{nullptr};
int recursionDepth = 0;
TypeId mkNegation(TypeId ty) const;
TypeId intersectFromParts(std::set<TypeId> parts);
TypeId intersectUnionWithType(TypeId left, TypeId right);
TypeId intersectUnions(TypeId left, TypeId right);
TypeId intersectNegatedUnion(TypeId left, TypeId right);
TypeId intersectTypeWithNegation(TypeId left, TypeId right);
TypeId intersectNegations(TypeId left, TypeId right);
TypeId intersectIntersectionWithType(TypeId left, TypeId right);
// Attempt to intersect the two types. Does not recurse. Does not handle
// unions, intersections, or negations.
std::optional<TypeId> basicIntersect(TypeId left, TypeId right);
std::optional<TypeId> basicIntersectWithTruthy(TypeId target) const;
std::optional<TypeId> basicIntersectWithFalsy(TypeId target) const;
TypeId intersect(TypeId left, TypeId right);
TypeId union_(TypeId left, TypeId right);
TypeId simplify(TypeId ty);
TypeId simplify(TypeId ty, DenseHashSet<TypeId>& seen);
std::optional<TypeId> intersectOne(TypeId target, TypeId discriminant) const;
std::optional<TypeId> subtractOne(TypeId target, TypeId discriminant) const;
std::optional<Property> intersectProperty(const Property& target, const Property& discriminant, DenseHashSet<TypeId>& seen) const;
std::optional<TypeId> intersectWithSimpleDiscriminant(TypeId target, TypeId discriminant, DenseHashSet<TypeId>& seen) const;
std::optional<TypeId> intersectWithSimpleDiscriminant(TypeId target, TypeId discriminant) const;
};
// Match the exact type false|nil
static bool isFalsyType_DEPRECATED(TypeId ty)
{
ty = follow(ty);
const UnionType* ut = get<UnionType>(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<PrimitiveType>(t); pt && pt->type == PrimitiveType::NilType)
hasNil = true;
else if (auto st = get<SingletonType>(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<PrimitiveType>(t); pt && pt->type == PrimitiveType::NilType)
hasNil = true;
else if (auto st = get<SingletonType>(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_DEPRECATED(TypeId ty)
{
ty = follow(ty);
const NegationType* nt = get<NegationType>(ty);
if (!nt)
return false;
return isFalsyType_DEPRECATED(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;
}
break;
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;
}
break;
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;
}
break;
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;
}
break;
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;
}
break;
}
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<FreeType>(ty) || get<GenericType>(ty) || get<BlockedType>(ty) || get<PendingExpansionType>(ty);
}
Relation relate(TypeId left, TypeId right, SimplifierSeenSet& seen);
Relation relateTables(TypeId left, TypeId right, SimplifierSeenSet& seen)
{
NotNull<const TableType> leftTable{get<TableType>(left)};
NotNull<const TableType> rightTable{get<TableType>(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.count(prop.first) > 0;
}
);
bool foundPropFromRightInLeft = std::any_of(
begin(rightTable->props),
end(rightTable->props),
[&](auto prop)
{
return leftTable->props.count(prop.first) > 0;
}
);
if (!foundPropFromLeftInRight && !foundPropFromRightInLeft && leftTable->props.size() >= 1 && rightTable->props.size() >= 1)
return FFlag::LuauRelateTablesAreNeverDisjoint ? Relation::Intersects : 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;
if (!leftProp.isShared() || !rightProp.isShared())
return Relation::Intersects;
Relation r;
if (FFlag::LuauRemoveTypeCallsForReadWriteProps)
r = relate(*leftProp.readTy, *rightProp.readTy, seen);
else
r = relate(leftProp.type_DEPRECATED(), rightProp.type_DEPRECATED(), seen);
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, SimplifierSeenSet& seen)
{
// TODO nice to have: Relate functions of equal argument and return arity
left = follow(left);
right = follow(right);
if (left == right)
return Relation::Coincident;
std::pair<TypeId, TypeId> typePair{left, right};
if (!seen.insert(typePair))
{
// TODO: is this right at all?
// The thinking here is that this is a cycle if we get here, and therefore its coincident.
return Relation::Coincident;
}
if (get<UnknownType>(left))
{
if (get<AnyType>(right))
return Relation::Subset;
if (get<UnknownType>(right))
return Relation::Coincident;
if (get<ErrorType>(right))
return Relation::Disjoint;
return Relation::Superset;
}
if (get<UnknownType>(right))
return flip(relate(right, left, seen));
if (get<AnyType>(left))
{
if (get<AnyType>(right))
return Relation::Coincident;
return Relation::Superset;
}
if (get<AnyType>(right))
return flip(relate(right, left, seen));
// Type variables
// * FreeType
// * GenericType
// * BlockedType
// * PendingExpansionType
// Tops and bottoms
// * ErrorType
// * AnyType
// * NeverType
// * UnknownType
// Concrete
// * PrimitiveType
// * SingletonType
// * FunctionType
// * TableType
// * MetatableType
// * ExternType
// * UnionType
// * IntersectionType
// * NegationType
if (isTypeVariable(left) || isTypeVariable(right))
return Relation::Intersects;
if (FFlag::LuauSimplificationTableExternType)
{
// if either type is a type function, we cannot know if they'll be related.
if (get<TypeFunctionInstanceType>(left) || get<TypeFunctionInstanceType>(right))
return Relation::Intersects;
}
if (get<ErrorType>(left))
{
if (get<ErrorType>(right))
return Relation::Coincident;
else if (get<AnyType>(right))
return Relation::Subset;
return Relation::Disjoint;
}
else if (get<ErrorType>(right))
return flip(relate(right, left, seen));
if (get<NeverType>(left))
{
if (get<NeverType>(right))
return Relation::Coincident;
return Relation::Subset;
}
else if (get<NeverType>(right))
return flip(relate(right, left, seen));
if (auto ut = get<IntersectionType>(left))
return Relation::Intersects;
else if (auto ut = get<IntersectionType>(right))
return Relation::Intersects;
if (auto ut = get<UnionType>(left))
return Relation::Intersects;
else if (auto ut = get<UnionType>(right))
{
std::vector<Relation> opts;
for (TypeId part : ut)
{
Relation r = relate(left, part, seen);
if (r == Relation::Subset || r == Relation::Coincident)
return Relation::Subset;
}
return Relation::Intersects;
}
if (auto rnt = get<NegationType>(right))
{
Relation a = relate(left, rnt->ty, seen);
switch (a)
{
case Relation::Coincident:
// number & ~number
return Relation::Disjoint;
case Relation::Disjoint:
if (get<NegationType>(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<NegationType>(left))
return flip(relate(right, left, seen));
if (auto lp = get<PrimitiveType>(left))
{
if (auto rp = get<PrimitiveType>(right))
{
if (lp->type == rp->type)
return Relation::Coincident;
return Relation::Disjoint;
}
if (auto rs = get<SingletonType>(right))
{
if (lp->type == PrimitiveType::String && rs->variant.get_if<StringSingleton>())
return Relation::Superset;
if (lp->type == PrimitiveType::Boolean && rs->variant.get_if<BooleanSingleton>())
return Relation::Superset;
return Relation::Disjoint;
}
if (lp->type == PrimitiveType::Function)
{
if (get<FunctionType>(right))
return Relation::Superset;
return Relation::Disjoint;
}
if (lp->type == PrimitiveType::Table)
{
if (get<TableType>(right))
return Relation::Superset;
return Relation::Disjoint;
}
if (get<FunctionType>(right) || get<TableType>(right) || get<MetatableType>(right) || get<ExternType>(right))
return Relation::Disjoint;
}
if (auto ls = get<SingletonType>(left))
{
if (get<FunctionType>(right) || get<TableType>(right) || get<MetatableType>(right) || get<ExternType>(right))
return Relation::Disjoint;
if (get<PrimitiveType>(right))
return flip(relate(right, left, seen));
if (auto rs = get<SingletonType>(right))
{
if (ls->variant == rs->variant)
return Relation::Coincident;
return Relation::Disjoint;
}
}
if (get<FunctionType>(left))
{
if (auto rp = get<PrimitiveType>(right))
{
if (rp->type == PrimitiveType::Function)
return Relation::Subset;
return Relation::Disjoint;
}
return Relation::Intersects;
}
if (auto lt = get<TableType>(left))
{
if (auto rp = get<PrimitiveType>(right))
{
if (rp->type == PrimitiveType::Table)
return Relation::Subset;
return Relation::Disjoint;
}
if (auto rt = get<TableType>(right))
{
// TODO PROBABLY indexers and metatables.
if (1 == rt->props.size())
{
Relation r = relateTables(left, right, seen);
/*
* 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;
return r;
}
if (1 == lt->props.size())
return flip(relate(right, left, seen));
return Relation::Intersects;
}
if (FFlag::LuauSimplificationTableExternType)
{
if (auto re = get<ExternType>(right))
{
Relation overall = Relation::Coincident;
for (auto& [name, prop] : lt->props)
{
if (auto propInExternType = re->props.find(name); propInExternType != re->props.end())
{
Relation propRel;
if (FFlag::LuauRemoveTypeCallsForReadWriteProps)
{
LUAU_ASSERT(prop.readTy && propInExternType->second.readTy);
propRel = relate(*prop.readTy, *propInExternType->second.readTy);
}
else
propRel = relate(prop.type_DEPRECATED(), propInExternType->second.type_DEPRECATED());
if (propRel == Relation::Disjoint)
return Relation::Disjoint;
if (propRel == Relation::Coincident)
continue;
overall = Relation::Intersects;
}
}
return overall;
}
}
// TODO metatables
return Relation::Disjoint;
}
if (auto ct = get<ExternType>(left))
{
if (auto rct = get<ExternType>(right))
{
if (isSubclass(ct, rct))
return Relation::Subset;
if (isSubclass(rct, ct))
return Relation::Superset;
return Relation::Disjoint;
}
if (FFlag::LuauRefineTablesWithReadType && is<TableType>(right))
{
// FIXME: This could be better in that we can say a table only
// intersects with an extern type if they share a property, but
// for now it is within the contract of the function to claim
// the two intersect.
return Relation::Intersects;
}
return Relation::Disjoint;
}
return Relation::Intersects;
}
// A cheap and approximate subtype test
Relation relate(TypeId left, TypeId right)
{
SimplifierSeenSet seen{{}};
return relate(left, right, seen);
}
TypeId TypeSimplifier::mkNegation(TypeId ty) const
{
TypeId result = nullptr;
if (ty == builtinTypes->truthyType)
result = builtinTypes->falsyType;
else if (ty == builtinTypes->falsyType)
result = builtinTypes->truthyType;
else if (auto ntv = get<NegationType>(ty))
result = follow(ntv->ty);
else
result = arena->addType(NegationType{ty});
return result;
}
TypeId TypeSimplifier::intersectFromParts(std::set<TypeId> 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<IntersectionType>(t))
{
for (TypeId part : ut)
parts.insert(part);
parts.erase(copy);
}
}
}
std::set<TypeId> 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<TypeId> 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<TypeId>{begin(newParts), end(newParts)}});
}
TypeId TypeSimplifier::intersectUnionWithType(TypeId left, TypeId right)
{
const UnionType* leftUnion = get<UnionType>(left);
LUAU_ASSERT(leftUnion);
bool changed = false;
std::set<TypeId> newParts;
size_t maxSize = DFInt::LuauSimplificationComplexityLimit;
if (leftUnion->options.size() > maxSize)
return arena->addType(IntersectionType{{left, right}});
for (TypeId part : leftUnion)
{
TypeId simplified = intersect(right, part);
changed |= simplified != part;
if (get<NeverType>(simplified))
{
changed = true;
continue;
}
newParts.insert(simplified);
// Initial combination size check could not predict nested union iteration
if (newParts.size() > maxSize)
return arena->addType(IntersectionType{{left, right}});
}
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<TypeId>(begin(newParts), end(newParts))});
}
TypeId TypeSimplifier::intersectUnions(TypeId left, TypeId right)
{
const UnionType* leftUnion = get<UnionType>(left);
LUAU_ASSERT(leftUnion);
const UnionType* rightUnion = get<UnionType>(right);
LUAU_ASSERT(rightUnion);
std::set<TypeId> newParts;
// Combinatorial blowup moment!!
// combination size
size_t optionSize = (int)leftUnion->options.size() * rightUnion->options.size();
size_t maxSize = DFInt::LuauSimplificationComplexityLimit;
if (optionSize > maxSize)
return arena->addType(IntersectionType{{left, right}});
for (TypeId leftPart : leftUnion)
{
for (TypeId rightPart : rightUnion)
{
TypeId simplified = intersect(leftPart, rightPart);
if (get<NeverType>(simplified))
continue;
newParts.insert(simplified);
// Initial combination size check could not predict nested union iteration
if (newParts.size() > maxSize)
return arena->addType(IntersectionType{{left, right}});
}
}
if (newParts.empty())
return builtinTypes->neverType;
else if (newParts.size() == 1)
return *begin(newParts);
else
return arena->addType(UnionType{std::vector<TypeId>(begin(newParts), end(newParts))});
}
TypeId TypeSimplifier::intersectNegatedUnion(TypeId left, TypeId right)
{
// ~(A | B) & C
// (~A & C) & (~B & C)
const NegationType* leftNegation = get<NegationType>(left);
LUAU_ASSERT(leftNegation);
TypeId negatedTy = follow(leftNegation->ty);
const UnionType* negatedUnion = get<UnionType>(negatedTy);
LUAU_ASSERT(negatedUnion);
bool changed = false;
std::set<TypeId> 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<NeverType>(simplified))
changed = true;
else
newParts.insert(simplified);
break;
}
}
if (!changed)
return right;
else
return intersectFromParts(std::move(newParts));
}
std::optional<TypeId> TypeSimplifier::basicIntersectWithTruthy(TypeId target) const
{
target = follow(target);
if (FFlag::LuauRefineTablesWithReadType)
{
if (isApproximatelyTruthyType(target))
return target;
if (isApproximatelyFalsyType(target))
return builtinTypes->neverType;
}
if (is<UnknownType>(target))
return builtinTypes->truthyType;
if (is<AnyType>(target))
// any = *error-type* | unknown, so truthy & any = *error-type* | truthy
return arena->addType(UnionType{{builtinTypes->truthyType, builtinTypes->errorType}});
if (is<NeverType, ErrorType>(target))
return target;
if (is<FunctionType, TableType, MetatableType, ExternType>(target))
return target;
if (auto pt = get<PrimitiveType>(target))
{
switch (pt->type)
{
case PrimitiveType::NilType:
return builtinTypes->neverType;
case PrimitiveType::Boolean:
return builtinTypes->trueType;
default:
return target;
}
}
if (auto st = get<SingletonType>(target))
return st->variant == BooleanSingleton{false} ? builtinTypes->neverType : target;
return std::nullopt;
}
std::optional<TypeId> TypeSimplifier::basicIntersectWithFalsy(TypeId target) const
{
target = follow(target);
if (FFlag::LuauRefineTablesWithReadType)
{
if (isApproximatelyTruthyType(target))
return builtinTypes->neverType;
if (isApproximatelyFalsyType(target))
return target;
}
if (is<NeverType, ErrorType>(target))
return target;
if (is<AnyType>(target))
// any = *error-type* | unknown, so falsy & any = *error-type* | falsy
return arena->addType(UnionType{{builtinTypes->falsyType, builtinTypes->errorType}});
if (is<UnknownType>(target))
return builtinTypes->falsyType;
if (is<FunctionType, TableType, MetatableType, ExternType>(target))
return builtinTypes->neverType;
if (auto pt = get<PrimitiveType>(target))
{
switch (pt->type)
{
case PrimitiveType::NilType:
return builtinTypes->nilType;
case PrimitiveType::Boolean:
return builtinTypes->falseType;
default:
return builtinTypes->neverType;
}
}
if (auto st = get<SingletonType>(target))
return st->variant == BooleanSingleton{false} ? builtinTypes->falseType : builtinTypes->neverType;
return std::nullopt;
}
TypeId TypeSimplifier::intersectTypeWithNegation(TypeId left, TypeId right)
{
const NegationType* leftNegation = get<NegationType>(left);
LUAU_ASSERT(leftNegation);
TypeId negatedTy = follow(leftNegation->ty);
if (negatedTy == right)
return builtinTypes->neverType;
if (auto ut = get<UnionType>(negatedTy))
{
// ~(A | B) & C
// (~A & C) & (~B & C)
bool changed = false;
std::set<TypeId> 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<UnionType>(right))
{
// ~A & (B | C)
bool changed = false;
std::set<TypeId> 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<TypeId>{begin(newParts), end(newParts)}});
}
if (auto pt = get<PrimitiveType>(right); pt && pt->type == PrimitiveType::Boolean)
{
if (auto st = get<SingletonType>(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<NegationType>(left);
LUAU_ASSERT(leftNegation);
if (get<UnionType>(follow(leftNegation->ty)))
return intersectNegatedUnion(left, right);
const NegationType* rightNegation = get<NegationType>(right);
LUAU_ASSERT(rightNegation);
if (get<UnionType>(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<IntersectionType>(left);
LUAU_ASSERT(leftIntersection);
if (leftIntersection->parts.size() > (size_t)DFInt::LuauSimplificationComplexityLimit)
return arena->addType(IntersectionType{{left, right}});
bool changed = false;
std::set<TypeId> 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<TypeId> TypeSimplifier::basicIntersect(TypeId left, TypeId right)
{
left = follow(left);
right = follow(right);
if (get<AnyType>(left) && get<ErrorType>(right))
return right;
if (get<AnyType>(right) && get<ErrorType>(left))
return left;
if (get<AnyType>(left))
return arena->addType(UnionType{{right, builtinTypes->errorType}});
if (get<AnyType>(right))
return arena->addType(UnionType{{left, builtinTypes->errorType}});
if (get<UnknownType>(left))
return right;
if (get<UnknownType>(right))
return left;
if (get<NeverType>(left))
return left;
if (get<NeverType>(right))
return right;
if (auto pt = get<PrimitiveType>(left); pt && pt->type == PrimitiveType::Boolean)
{
if (auto st = get<SingletonType>(right); st && st->variant.get_if<BooleanSingleton>())
return right;
if (auto nt = get<NegationType>(right))
{
if (auto st = get<SingletonType>(follow(nt->ty)); st && st->variant.get_if<BooleanSingleton>())
{
if (st->variant == BooleanSingleton{true})
return builtinTypes->falseType;
else
return builtinTypes->trueType;
}
}
}
else if (auto pt = get<PrimitiveType>(right); pt && pt->type == PrimitiveType::Boolean)
{
if (auto st = get<SingletonType>(left); st && st->variant.get_if<BooleanSingleton>())
return left;
if (auto nt = get<NegationType>(left))
{
if (auto st = get<SingletonType>(follow(nt->ty)); st && st->variant.get_if<BooleanSingleton>())
{
if (st->variant == BooleanSingleton{true})
return builtinTypes->falseType;
else
return builtinTypes->trueType;
}
}
}
if (const auto [lt, rt] = get2<TableType, TableType>(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() && leftProp.isShared() && it->second.isShared())
{
Relation r;
if (FFlag::LuauRemoveTypeCallsForReadWriteProps)
r = relate(*leftProp.readTy, *it->second.readTy);
else
r = relate(leftProp.type_DEPRECATED(), it->second.type_DEPRECATED());
switch (r)
{
case Relation::Disjoint:
return builtinTypes->neverType;
case Relation::Superset:
case Relation::Coincident:
return right;
case Relation::Subset:
if (1 == rt->props.size())
return left;
break;
default:
break;
}
}
}
else if (1 == rt->props.size())
return basicIntersect(right, left);
// If two tables have disjoint properties and indexers, we can combine them.
if (!lt->indexer && !rt->indexer && lt->state == TableState::Sealed && rt->state == TableState::Sealed)
{
if (rt->props.empty())
return left;
bool areDisjoint = true;
for (const auto& [name, leftProp] : lt->props)
{
if (rt->props.count(name))
{
areDisjoint = false;
break;
}
}
if (areDisjoint)
{
TableType::Props mergedProps = lt->props;
for (const auto& [name, rightProp] : rt->props)
mergedProps[name] = rightProp;
return arena->addType(TableType{mergedProps, std::nullopt, TypeLevel{}, lt->scope, TableState::Sealed});
}
}
return std::nullopt;
}
if (FFlag::LuauRefineTablesWithReadType)
{
if (isApproximatelyTruthyType(left))
if (auto res = basicIntersectWithTruthy(right))
return res;
if (isApproximatelyTruthyType(right))
if (auto res = basicIntersectWithTruthy(left))
return res;
if (isApproximatelyFalsyType(left))
if (auto res = basicIntersectWithFalsy(right))
return res;
if (isApproximatelyFalsyType(right))
if (auto res = basicIntersectWithFalsy(left))
return res;
}
else
{
if (isTruthyType_DEPRECATED(left))
if (auto res = basicIntersectWithTruthy(right))
return res;
if (isTruthyType_DEPRECATED(right))
if (auto res = basicIntersectWithTruthy(left))
return res;
if (isFalsyType_DEPRECATED(left))
if (auto res = basicIntersectWithFalsy(right))
return res;
if (isFalsyType_DEPRECATED(right))
if (auto res = basicIntersectWithFalsy(left))
return res;
}
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 (left == right)
return left;
if (get<AnyType>(left) && get<ErrorType>(right))
return right;
if (get<AnyType>(right) && get<ErrorType>(left))
return left;
if (get<UnknownType>(left) && !get<ErrorType>(right))
return right;
if (get<UnknownType>(right) && !get<ErrorType>(left))
return left;
if (get<AnyType>(left))
return arena->addType(UnionType{{right, builtinTypes->errorType}});
if (get<AnyType>(right))
return arena->addType(UnionType{{left, builtinTypes->errorType}});
if (get<UnknownType>(left))
return right;
if (get<UnknownType>(right))
return left;
if (get<NeverType>(left))
return left;
if (get<NeverType>(right))
return right;
if (auto lf = get<FreeType>(left))
{
Relation r = relate(lf->upperBound, right);
if (r == Relation::Subset || r == Relation::Coincident)
return left;
}
else if (auto rf = get<FreeType>(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<UnionType>(left))
{
if (get<UnionType>(right))
return intersectUnions(left, right);
else
return intersectUnionWithType(left, right);
}
else if (auto ut = get<UnionType>(right))
return intersectUnionWithType(right, left);
if (auto it = get<IntersectionType>(left))
return intersectIntersectionWithType(left, right);
else if (auto it = get<IntersectionType>(right))
return intersectIntersectionWithType(right, left);
if (get<NegationType>(left))
{
if (get<NegationType>(right))
return intersectNegations(left, right);
else
return intersectTypeWithNegation(left, right);
}
else if (get<NegationType>(right))
return intersectTypeWithNegation(right, left);
std::optional<TypeId> 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<NeverType>(left))
return right;
if (get<NeverType>(right))
return left;
if (auto leftUnion = get<UnionType>(left))
{
bool changed = false;
std::set<TypeId> newParts;
for (TypeId part : leftUnion)
{
if (get<NeverType>(part))
{
changed = true;
continue;
}
Relation r = relate(part, right);
switch (r)
{
case Relation::Coincident:
case Relation::Superset:
return left;
case Relation::Subset:
newParts.insert(right);
changed = true;
break;
default:
newParts.insert(part);
newParts.insert(right);
changed = true;
break;
}
}
if (!changed)
return left;
if (0 == newParts.size())
{
// If the left-side is changed but has no parts, then the left-side union is uninhabited.
return right;
}
else if (1 == newParts.size())
return *begin(newParts);
else
return arena->addType(UnionType{std::vector<TypeId>{begin(newParts), end(newParts)}});
}
else if (get<UnionType>(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<SingletonType>(left))
{
if (auto abs = as->variant.get_if<BooleanSingleton>())
{
if (auto bs = get<SingletonType>(right))
{
if (auto bbs = bs->variant.get_if<BooleanSingleton>())
{
if (abs->value != bbs->value)
return builtinTypes->booleanType;
}
}
}
}
return arena->addType(UnionType{{left, right}});
}
TypeId TypeSimplifier::simplify(TypeId ty)
{
DenseHashSet<TypeId> seen{nullptr};
return simplify(ty, seen);
}
TypeId TypeSimplifier::simplify(TypeId ty, DenseHashSet<TypeId>& seen)
{
RecursionLimiter limiter(&recursionDepth, 60);
ty = follow(ty);
if (seen.find(ty))
return ty;
seen.insert(ty);
if (auto nt = get<NegationType>(ty))
{
TypeId negatedTy = follow(nt->ty);
if (get<AnyType>(negatedTy))
return arena->addType(UnionType{{builtinTypes->neverType, builtinTypes->errorType}});
else if (get<UnknownType>(negatedTy))
return builtinTypes->neverType;
else if (get<NeverType>(negatedTy))
return builtinTypes->unknownType;
if (auto nnt = get<NegationType>(negatedTy))
return simplify(nnt->ty, seen);
}
// Promote {x: never} to never
if (auto tt = get<TableType>(ty))
{
if (1 == tt->props.size())
{
if (std::optional<TypeId> readTy = begin(tt->props)->second.readTy)
{
TypeId propTy = simplify(*readTy, seen);
if (get<NeverType>(propTy))
return builtinTypes->neverType;
}
}
}
return ty;
}
namespace
{
bool isSimpleDiscriminant(TypeId ty, DenseHashSet<TypeId>& seen)
{
ty = follow(ty);
// If we *ever* see a recursive type, bail right away, clearly that is
// not simple.
if (seen.contains(ty))
return false;
seen.insert(ty);
// NOTE: We could probably support `{}` as a simple discriminant.
if (auto ttv = get<TableType>(ty); ttv && ttv->props.size() == 1 && !ttv->indexer)
{
auto prop = begin(ttv->props)->second;
return (!prop.readTy || isSimpleDiscriminant(*prop.readTy, seen)) && (!prop.writeTy || isSimpleDiscriminant(*prop.writeTy, seen));
}
if (auto nt = get<NegationType>(ty))
return isSimpleDiscriminant(nt->ty, seen);
return is<PrimitiveType, SingletonType, ExternType>(ty) || isApproximatelyTruthyType(ty) || isApproximatelyFalsyType(ty);
}
/**
* There are some types that are "simple", and thus easy to intersect against:
* - The "truthy" (`~(false?)`) and "falsy" (`false?`) types are simple.
* - Primitive types, singleton types, and extern types are simple
* - Table types are simple if they have no indexer, and have a single property
* who's read and write types are also simple.
* - Cyclic types are never simple.
*/
bool isSimpleDiscriminant(TypeId ty)
{
DenseHashSet<TypeId> seenSet{nullptr};
return isSimpleDiscriminant(ty, seenSet);
}
} // namespace
std::optional<TypeId> TypeSimplifier::intersectOne(TypeId target, TypeId discriminant) const
{
switch (relate(target, discriminant))
{
case Relation::Disjoint: // No A is a B or vice versa
return builtinTypes->neverType;
case Relation::Subset: // Every A is in B
case Relation::Coincident: // Every A is in B and vice versa
return target;
case Relation::Superset: // Every B is in A
return discriminant;
case Relation::Intersects:
default:
// Some As are in B and some Bs are in A. ex (number | string) <-> (string | boolean).
return std::nullopt;
}
}
std::optional<TypeId> TypeSimplifier::subtractOne(TypeId target, TypeId discriminant) const
{
target = follow(target);
discriminant = follow(discriminant);
if (auto nt = get<NegationType>(discriminant))
return intersectOne(target, nt->ty);
switch (relate(target, discriminant))
{
case Relation::Disjoint: // A v B is empty => A - B is equivalent to A
return target;
case Relation::Subset: // A v B is A => A - B is empty
case Relation::Coincident: // Same as above: A == B so A - B = {}
return builtinTypes->neverType;
case Relation::Superset:
case Relation::Intersects:
default:
return std::nullopt;
}
}
std::optional<Property> TypeSimplifier::intersectProperty(const Property& target, const Property& discriminant, DenseHashSet<TypeId>& seen) const
{
// NOTE: I invite the reader to refactor the below code as a fun coding
// exercise. It looks ugly to me, but I don't think we can make it
// any cleaner.
Property prop;
prop.deprecated = target.deprecated || discriminant.deprecated;
// We're trying to follow the following rules for both read and write types:
// * If the type is present on both properties, intersect it, and return
// `std::nullopt` if we fail.
// * If the type only exists on one property or the other, take that.
if (target.readTy && discriminant.readTy)
{
prop.readTy = intersectWithSimpleDiscriminant(*target.readTy, *discriminant.readTy, seen);
if (!prop.readTy)
return std::nullopt;
}
else if (target.readTy && !discriminant.readTy)
prop.readTy = target.readTy;
else if (!target.readTy && discriminant.readTy)
prop.readTy = discriminant.readTy;
if (target.writeTy && discriminant.writeTy)
{
prop.writeTy = intersectWithSimpleDiscriminant(*target.writeTy, *discriminant.writeTy, seen);
if (!prop.writeTy)
return std::nullopt;
}
else if (target.writeTy && !discriminant.writeTy)
prop.writeTy = target.writeTy;
else if (!target.writeTy && discriminant.writeTy)
prop.writeTy = discriminant.writeTy;
return {prop};
}
std::optional<TypeId> TypeSimplifier::intersectWithSimpleDiscriminant(TypeId target, TypeId discriminant, DenseHashSet<TypeId>& seen) const
{
if (seen.contains(target))
return std::nullopt;
target = follow(target);
discriminant = follow(discriminant);
if (auto ut = get<UnionType>(target))
{
seen.insert(target);
TypeIds options;
for (TypeId option : ut)
{
auto result = intersectWithSimpleDiscriminant(option, discriminant, seen);
if (!result)
return std::nullopt;
if (is<UnknownType>(result))
return builtinTypes->unknownType;
if (!is<NeverType>(*result))
options.insert(*result);
}
if (options.empty())
return builtinTypes->neverType;
if (options.size() == 1)
return *options.begin();
return arena->addType(UnionType{options.take()});
}
if (auto it = get<IntersectionType>(target))
{
seen.insert(target);
TypeIds parts;
for (TypeId part : it)
{
auto result = intersectWithSimpleDiscriminant(part, discriminant, seen);
if (!result)
return std::nullopt;
if (is<NeverType>(*result))
return builtinTypes->neverType;
if (auto subIntersection = get<IntersectionType>(*result))
{
for (TypeId subOption : subIntersection)
{
if (is<NeverType>(subOption))
return builtinTypes->neverType;
if (!is<UnknownType>(result))
parts.insert(*result);
}
}
else if (!is<UnknownType>(*result))
parts.insert(*result);
}
if (parts.empty())
return builtinTypes->unknownType;
if (parts.size() == 1)
return *parts.begin();
return arena->addType(IntersectionType{parts.take()});
}
if (auto ttv = get<TableType>(target))
{
if (auto discTtv = get<TableType>(discriminant))
{
// The precondition of this function is that `discriminant` is
// simple, so if it's a table it *must* be a sealed table with
// a single property and no indexer.
LUAU_ASSERT(discTtv->props.size() == 1 && !discTtv->indexer);
const auto discProp = begin(discTtv->props);
if (auto tyProp = ttv->props.find(discProp->first); tyProp != ttv->props.end())
{
auto property = intersectProperty(tyProp->second, discProp->second, seen);
if (!property)
return std::nullopt;
if (property->readTy && is<NeverType>(follow(property->readTy)))
return builtinTypes->neverType;
if (property->writeTy && is<NeverType>(follow(property->writeTy)))
return builtinTypes->neverType;
// If the property we get back is pointer identical to the
// original property, return the underlying property as an
// optimization.
if (tyProp->second.readTy == property->readTy && tyProp->second.writeTy == property->writeTy)
return target;
CloneState cs{builtinTypes};
TypeId result = shallowClone(target, *arena, cs, /* clonePersistentTypes */ true);
auto resultTtv = getMutable<TableType>(result);
LUAU_ASSERT(resultTtv);
resultTtv->props[tyProp->first] = *property;
// Shallow cloning clears out scopes, so let's put back the
// scope from the original type.
resultTtv->scope = ttv->scope;
return result;
}
CloneState cs{builtinTypes};
TypeId result = shallowClone(target, *arena, cs, /* clonePersistentTypes */ true);
// Shallow cloning clears out scopes, so let's put back the
// scope from the original type.
auto resultTtv = getMutable<TableType>(result);
LUAU_ASSERT(resultTtv);
resultTtv->props.emplace(discProp->first, discProp->second);
resultTtv->scope = ttv->scope;
return result;
}
// At this point, we're doing something like:
//
// { ... } & ~nil
//
// Which can be handled via fallthrough.
}
// FIXME: We could probably return to this.
if (is<FreeType, GenericType, BlockedType, PendingExpansionType, TypeFunctionInstanceType>(target))
return std::nullopt;
if (isApproximatelyTruthyType(discriminant))
return basicIntersectWithTruthy(target);
if (isApproximatelyTruthyType(target))
return basicIntersectWithTruthy(discriminant);
if (isApproximatelyFalsyType(discriminant))
return basicIntersectWithFalsy(target);
if (isApproximatelyFalsyType(target))
return basicIntersectWithFalsy(discriminant);
if (is<AnyType>(target))
return arena->addType(UnionType{{builtinTypes->errorType, discriminant}});
if (auto nty = get<NegationType>(discriminant))
return subtractOne(target, nty->ty);
return intersectOne(target, discriminant);
}
std::optional<TypeId> TypeSimplifier::intersectWithSimpleDiscriminant(TypeId target, TypeId discriminant) const
{
DenseHashSet<TypeId> seenSet{nullptr};
return intersectWithSimpleDiscriminant(target, discriminant, seenSet);
}
SimplifyResult simplifyIntersection(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId left, TypeId right)
{
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 simplifyIntersection(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, std::set<TypeId> parts)
{
TypeSimplifier s{builtinTypes, arena};
TypeId res = s.intersectFromParts(std::move(parts));
return SimplifyResult{res, std::move(s.blockedTypes)};
}
SimplifyResult simplifyUnion(NotNull<BuiltinTypes> builtinTypes, NotNull<TypeArena> arena, TypeId left, TypeId right)
{
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)};
}
std::optional<TypeId> intersectWithSimpleDiscriminant(
NotNull<BuiltinTypes> builtinTypes,
NotNull<TypeArena> arena,
TypeId target,
TypeId discriminant
)
{
if (!isSimpleDiscriminant(discriminant))
{
if (isSimpleDiscriminant(target))
return intersectWithSimpleDiscriminant(builtinTypes, arena, discriminant, target);
return std::nullopt;
}
TypeSimplifier s{builtinTypes, arena};
return s.intersectWithSimpleDiscriminant(target, discriminant);
}
} // namespace Luau