Fix recursive function type error with for-in loops (#2216)#2220
Closed
Definisi wants to merge 5 commits intoluau-lang:masterfrom
Closed
Fix recursive function type error with for-in loops (#2216)#2220Definisi wants to merge 5 commits intoluau-lang:masterfrom
Definisi wants to merge 5 commits intoluau-lang:masterfrom
Conversation
Fix the "Type function instance intersect<blocked, ~nil> is uninhabited" error that occurred when a recursive function used a for-in loop over its own recursive call result. The issue was in ConstraintGenerator::lookup - when looking up a type for a phi DefId, the function would return a prepopulated BlockedType instead of resolving through the phi operands to find the actual function signature. Changes: - Modified lookup() to prefer resolving phi types through operands first for single-operand phis before falling back to direct binding lookup - Fixed occurs() in BuiltinTypeFunctions.cpp to check IntersectionType instead of UnionType - Added recursive detection for intersect type function to handle degenerate recursive types Added test case for the exact pattern from issue luau-lang#2216.
Only prefer resolving through the phi operand when the phi itself is bound to a BlockedType. This is more conservative and avoids changing behavior for cases where the phi has a concrete type. Previously, we always tried the operand first for single-operand phis, which could change error reporting behavior for functions without annotations.
Changed GlobalPrepopulator::visit(AstExprGlobal*) to only set lvalueTypes for Cell defs, not Phi defs. Phi defs represent captured references (like recursive function calls) and should resolve through their operands to get the actual function type, not a prepopulated BlockedType. This is a more targeted fix that addresses the root cause: GlobalPrepopulator was treating recursive references the same as forward references, but recursive references should use the actual function signature being defined, not a placeholder BlockedType. Reverted the lookup() changes to keep the original behavior.
When looking up a single-operand phi that has a BlockedType binding (from GlobalPrepopulator), check if the operand has a concrete FunctionType. If so, prefer the operand to get the actual function signature instead of the placeholder BlockedType. This specifically handles recursive function calls where the function has explicit type annotations - the operand will have the FunctionType with the proper signature, while the phi's direct binding is a BlockedType from prepopulation. For functions without explicit annotations (like those with @deprecated), the operand won't be a FunctionType, so we fall back to the phi's binding which will be properly resolved with deprecation info.
When the intersect type function encounters pending (blocked) types, instead of returning null and blocking entirely, return an IntersectionType that can be simplified later. This allows recursive function calls to proceed even when the return type is temporarily blocked. This prevents the "uninhabited type function" error for recursive functions with explicit return type annotations used in for-in loops.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes #2216 - "Recursive Function Type Error"
This PR fixes the error
Type function instance intersect<blocked-XXXXX, ~nil> is uninhabitedthat occurred when a recursive function used a for-in loop over its own recursive call result.Problem
The following code would produce a confusing type error:
Before Fix (Error Output)
After Fix (No Error)
The code type-checks successfully with no errors.
Root Cause
The issue was in
ConstraintGenerator::lookup(). When looking up a type for a phi DefId (which represents captured variables across function boundaries):GlobalPrepopulatorsetslvalueTypes[phiDef] = BlockedTypefor all global references during prepopulationlookupfunction first checked if the phi was directly bound in the scopeBlockedTypefrom prepopulation and returned it immediatelyThe
BlockedTypenever got resolved because of a dependency cycle, causing theintersect<blocked, ~nil>type function to fail.Fix
Modified
ConstraintGenerator::lookup()to prefer resolving phi types through their operands first (for single-operand phis), before falling back to direct binding lookup:This ensures that for recursive function references, the lookup correctly resolves through the phi operand (which points to the Cell DefId bound to the function signature) instead of returning the prepopulated
BlockedType.Additional Fixes
occurs()inBuiltinTypeFunctions.cppwhich was incorrectly checking forUnionTypeinstead ofIntersectionTypeintersecttype function to handle degenerate recursive typesTest Plan
oss_2216_recursive_function_with_for_in_loopthat verifies the exact code pattern from the issue