Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 164 additions & 2 deletions Analysis/src/BuiltinTypeFunctions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1331,7 +1331,7 @@ bool occurs(TypeId haystack, TypeId needle, DenseHashSet<TypeId>& seen)
return true;
}

if (auto it = get<UnionType>(haystack))
if (auto it = get<IntersectionType>(haystack))
{
for (auto part : it)
if (occurs(part, needle, seen))
Expand Down Expand Up @@ -1703,6 +1703,141 @@ TypeFunctionReductionResult<TypeId> unionTypeFunction(
}


namespace
{

// Check if 'needle' occurs anywhere within 'haystack' (for detecting recursive type references)
bool occursInIntersect(TypeId haystack, TypeId needle, DenseHashSet<TypeId>& seen)
{
haystack = follow(haystack);
needle = follow(needle);

if (needle == haystack)
return true;

if (seen.contains(haystack))
return false;

seen.insert(haystack);

if (auto ut = get<UnionType>(haystack))
{
for (auto option : ut)
if (occursInIntersect(option, needle, seen))
return true;
}

if (auto it = get<IntersectionType>(haystack))
{
for (auto part : it)
if (occursInIntersect(part, needle, seen))
return true;
}

return false;
}

bool occursInIntersect(TypeId haystack, TypeId needle)
{
DenseHashSet<TypeId> seen{nullptr};
return occursInIntersect(haystack, needle, seen);
}

// Substitution class to scrub recursive type references from intersection type parameters
struct IntersectTypeScrubber : public Substitution
{
NotNull<TypeFunctionContext> ctx;
TypeId needle;

explicit IntersectTypeScrubber(NotNull<TypeFunctionContext> ctx, TypeId needle)
: Substitution(ctx->arena)
, ctx{ctx}
, needle{needle}
{
}

bool isDirty(TypePackId tp) override
{
return false;
}

bool ignoreChildren(TypePackId tp) override
{
return false;
}

TypePackId clean(TypePackId tp) override
{
return tp;
}

bool isDirty(TypeId ty) override
{
if (auto ut = get<UnionType>(ty))
{
for (auto option : ut)
{
if (option == needle)
return true;
}
}
else if (auto it = get<IntersectionType>(ty))
{
for (auto part : it)
{
if (part == needle)
return true;
}
}
return ty == needle;
}

bool ignoreChildren(TypeId ty) override
{
return !is<UnionType, IntersectionType>(ty);
}

TypeId clean(TypeId ty) override
{
if (auto ut = get<UnionType>(ty))
{
TypeIds newOptions;
for (auto option : ut)
{
if (option != needle && !is<NeverType>(option))
newOptions.insert(option);
}
if (newOptions.empty())
return ctx->builtins->neverType;
else if (newOptions.size() == 1)
return *newOptions.begin();
else
return ctx->arena->addType(UnionType{newOptions.take()});
}
else if (auto it = get<IntersectionType>(ty))
{
TypeIds newParts;
for (auto part : it)
{
if (part != needle && !is<UnknownType>(part))
newParts.insert(part);
}
if (newParts.empty())
return ctx->builtins->unknownType;
else if (newParts.size() == 1)
return *newParts.begin();
else
return ctx->arena->addType(IntersectionType{newParts.take()});
}
else if (ty == needle)
return ctx->builtins->unknownType;
else
return ty;
}
};

} // namespace

TypeFunctionReductionResult<TypeId> intersectTypeFunction(
TypeId instance,
const std::vector<TypeId>& typeParams,
Expand Down Expand Up @@ -1732,16 +1867,43 @@ TypeFunctionReductionResult<TypeId> intersectTypeFunction(
else if (types.size() == 2 && get<NoRefineType>(types[0]))
return {types[1], Reduction::MaybeOk, {}, {}};

// If we end up minting an intersect type like:
//
// t1 where t1 = intersect<T | t1, Y>
//
// This can create a degenerate recursive type. Instead, we clip the recursive part
// by removing occurrences of the instance from the type parameters.
for (size_t i = 0; i < types.size(); i++)
{
if (occursInIntersect(types[i], instance))
{
IntersectTypeScrubber its{ctx, instance};
if (auto result = its.substitute(types[i]))
types[i] = *result;
}
}

// check to see if the operand types are resolved enough, and wait to reduce if not
// if any of them are `never`, the intersection will always be `never`, so we can reduce directly.
std::vector<TypeId> pendingTypes;
for (auto ty : types)
{
if (isPending(ty, ctx->solver))
return {std::nullopt, Reduction::MaybeOk, {ty}, {}};
pendingTypes.push_back(ty);
else if (get<NeverType>(ty))
return {ctx->builtins->neverType, Reduction::MaybeOk, {}, {}};
}

// If there are pending types, we can't fully reduce yet.
// Return an IntersectionType that can be simplified later, rather than blocking.
// This handles recursive function calls where the return type is blocked but known.
if (!pendingTypes.empty())
{
// Return the intersection as-is, with pending types noted for later resolution
TypeId intersection = ctx->arena->addType(IntersectionType{types});
return {intersection, Reduction::MaybeOk, {pendingTypes.begin(), pendingTypes.end()}, {}};
}

// fold over the types with `simplifyIntersection`
TypeId resultTy = ctx->builtins->unknownType;
// collect types which caused intersection to return never
Expand Down
2 changes: 0 additions & 2 deletions Analysis/src/ConstraintGenerator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,6 @@ std::optional<TypeId> ConstraintGenerator::lookup(const ScopePtr& scope, Locatio
{
if (auto found = scope->lookup(def))
return *found;
else if (!prototype && phi->operands.size() == 1)
return lookup(scope, location, phi->operands.at(0), prototype);
else if (!prototype)
return std::nullopt;

Expand Down
33 changes: 33 additions & 0 deletions tests/TypeFunction.test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2095,4 +2095,37 @@ TEST_CASE_FIXTURE(BuiltinsFixture, "oss_2144_type_instantiation_on_type_function
CHECK_EQ("number", toString(requireType("_b")));
}

TEST_CASE_FIXTURE(BuiltinsFixture, "oss_2216_recursive_function_with_for_in_loop")
{
// Issue #2216: Recursive global function with for-in loop over recursive call result
// should not produce "Type function instance intersect<blocked, ~nil> is uninhabited" error
ScopedFastFlag sffs[] = {
{FFlag::LuauSolverV2, true},
};

CheckResult result = check(R"(
--!strict
type tb_any = {[any]:any}

function flatten(t: tb_any): tb_any
local out: tb_any = {}
for k, v in flatten(t) do
out[k] = v
end
return out
end
)");

// We should not get UninhabitedTypeFunction error for intersect
for (const auto& err : result.errors)
{
if (auto utf = get<UninhabitedTypeFunction>(err))
{
std::string typeStr = toString(utf->ty);
CHECK_MESSAGE(typeStr.find("intersect") == std::string::npos,
"Should not have uninhabited intersect type function: " << typeStr);
}
}
}

TEST_SUITE_END();
Loading