Skip to content

Different type-widening for indirect vs. direct recursion #45759

Closed
@martinholters

Description

@martinholters

This is good:

julia> f(x::Tuple{Any,Vararg}) = x[1] + f(x[2:end])
f (generic function with 1 method)

julia> f(x::Tuple{}) = 0
f (generic function with 2 methods)

julia> only(Base.return_types(f, Tuple{Tuple{Int,Int,Int,Int,Int,Int,Int}}))
Int64

However, when introducing a level of indirection, inference widens more aggressively when detecting the recursion and fails:

julia> g(x::Tuple{Any,Vararg}) = x[1] + _g(x[2:end])
g (generic function with 1 method)

julia> g(x::Tuple{}) = 0
g (generic function with 2 methods)

julia> _g(x) = g(x)
_g (generic function with 1 method)

julia> only(Base.return_types(g, Tuple{Tuple{Int,Int,Int,Int,Int,Int,Int}}))
Any

This is particularly unfortunate as this kind of indirection is implicitly introduced when adding kwargs:

julia> h(x::Tuple{Any,Vararg}; kwargs...) = x[1] + h(x[2:end]; kwargs...)
h (generic function with 1 method)

julia> h(x::Tuple{}; kwargs...) = 0
h (generic function with 2 methods)

julia> only(Base.return_types(h, Tuple{Tuple{Int,Int,Int,Int,Int,Int,Int}}))
Any

A manifestation of this effect is #45715.

We deliberately distinguish the cases of direct vs. indirect recursion:

if method === sv.linfo.def
# Under direct self-recursion, permit much greater use of reducers.
# here we assume that complexity(specTypes) :>= complexity(sig)
comparison = sv.linfo.specTypes
l_comparison = length((unwrap_unionall(comparison)::DataType).parameters)
spec_len = max(spec_len, l_comparison)
else
comparison = method.sig
end

I'd argue that we shouldn't do this and instead, treat both cases equal. As a first trial balloon, I did

--- a/base/compiler/abstractinterpretation.jl
+++ b/base/compiler/abstractinterpretation.jl
@@ -542,7 +542,7 @@ function abstract_call_method(interp::AbstractInterpreter, method::Method, @nosp
             l_comparison = length((unwrap_unionall(comparison)::DataType).parameters)
             spec_len = max(spec_len, l_comparison)
         else
-            comparison = method.sig
+            comparison = topmost.linfo.specTypes
         end
 
         if isdefined(method, :recursion_relation)

That indeed lets above cases all be inferred as Int (and also fixes #45715). However, one @inferred test in the broadcast tests then fails. Although I didn't really know what I was doing there, I was not expecting this to worsen inference results, but maybe it's now triggering some other edge case? Anyway, feedback from @vtjnash on why these two cases are treated differently and whether it's a good idea to try to unify them would be welcome.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions