Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Work around type intersection bug in _isequal, _isless, __inc, and __dec #42686

Merged
merged 2 commits into from
Oct 19, 2021

Conversation

martinholters
Copy link
Member

The underlying intersection bug looks like

julia> typeintersect(Tuple{Tuple{Any, Vararg{Any, N}}, Tuple{Any, Vararg{Any, N}}} where N, Tuple{Tuple{Int,Int,Int}, Tuple})
Tuple{Tuple{Int64, Int64, Int64}, NTuple{4, Any}}

It leads to the argument types of the functions with signatures of this type to be inferred wrongly, not actually matching the signature, which easily leads to problems down the road.

For the four methods touched here, the fact that the two tuple arguments must have the same length is not actually relevant for dispatch as they are internal functions which are only called if this actually holds. Also, this change will just replace the method error with another error if, for some reason, these functions would get called for tuples of unequal length. (For _isequal and _isless I've added type-assertions to ensure this, for __inc and __dec I'm not sure about the early returns, maybe someone who knows this code might take a look. Although it probably doesn't matter anyway.)

@aviatesk Can you confirm this preserves the benefits from #40594?

Fixes #42457. Closes #42682.

@Sacha0
Copy link
Member

Sacha0 commented Oct 18, 2021

Nice! Does an issue tracking the underlying intersection bug exist? :)

@Sacha0
Copy link
Member

Sacha0 commented Oct 18, 2021

Possibly #39088 (comment)?

@martinholters
Copy link
Member Author

Yes, that would be it. I meant to link to it but forgot, thanks for bringing it up.

test/tuple.jl Outdated
Comment on lines 676 to 677
f42457(a::Tuple{Int,Int,Int}, b::Tuple) = Base.isequal(a, Base.inferencebarrier(b)::Tuple)
@test f42457((1, 1, 1), (1, 1, 1))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test, as written, passes for me on master. This fails, but still seems indirect as a test:

Suggested change
f42457(a::Tuple{Int,Int,Int}, b::Tuple) = Base.isequal(a, Base.inferencebarrier(b)::Tuple)
@test f42457((1, 1, 1), (1, 1, 1))
f42457(a::Tuple{Int,Int,Int}, b::Tuple) = Base.isequal(a, Base.inferencebarrier(b)::NTuple)
@test f42457((1, 1, 1), (1, 1, 1))

More direct tests might be:

@test !isempty(methods(Base._isequal, (NTuple{3, Int}, Tuple)))

and

g42457(a, b) = Base.isequal(a, b) ? 1 : 2.0
@test only(return_types(f42457, (NTuple{3, Int}, Tuple))) === Union{Float64, Int}
@test only(return_types(f42457, (NTuple{3, Int}, NTuple))) === Union{Float64, Int}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need the ::Bool return type decl from the original issue to make it fail, and also yes more tests is good.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Added the ::Bool to the original test to have a test case closely derived from the issue and also added the tests proposed by @vtjnash. (Although I don't understand the intention of the !isempty(methods(...)) one, which also happens to pass on master.)

…and `__dec`

The underlying intersection bug (ref. #39088) looks like
```
julia> typeintersect(Tuple{Tuple{Any, Vararg{Any, N}}, Tuple{Any, Vararg{Any, N}}} where N, Tuple{Tuple{Int,Int,Int}, Tuple})
Tuple{Tuple{Int64, Int64, Int64}, NTuple{4, Any}}
```

It leads to the argument types of the functions with signatures of this
type to be inferred wrongly, not actually matching the signature, which
easily leads to problems down the road.

For the four methods touched here, the fact that the two tuple arguments
must have the same length is not actually relevant for dispatch as they
are internal functions which are only called if this actually holds.
Also, this change will just replace the method error with another error
if, for some reason, these fuctions would get called for tuples of
unequal length.
@aviatesk
Copy link
Member

Motivated by the following (false positive) error reports of JET, I wanted to introduce N variables because sometimes they can give get us extra (relational) type information for loosely-typed arguments.

# without `N` parameters
julia> using JET

julia> report_call((Any, Tuple{Any})) do t1, t2
           isequal(t1, t2) # or `Base._isequal(t1, t2)`
       end
═════ 1 possible error found ═════
┌ @ REPL[9]:2 Main.isequal(t1, t2)
│┌ @ tuple.jl:383 Base._isequal(t1, t2)
││┌ @ tuple.jl:389 Core.typeassert(t2, Core.apply_type(Base.Tuple, Base.Any, Core.apply_type(Base.Vararg, Base.Any)))
│││ invalid builtin function call: Core.typeassert(t2::Tuple{}, Core.apply_type(Base.Tuple, Base.Any, Core.apply_type(Base.Vararg, Base.Any)::Core.TypeofVararg)::Type{Tuple{Any, Vararg{Any}}})
││└────────────────

(so the idea is to use dispatch signature to narrow t1::Any to t1::Tuple{Any} when starting inference on _isequal(t1::Tuple{Any, Vararg{Any, N}, t2::Tuple{Any, Vararg{Any, N}))

For the four methods touched here, the fact that the two tuple arguments must have the same length is not actually relevant for dispatch as they are internal functions which are only called if this actually holds.

Yes, the "improvement" from #40594 rarely benefits runtime, it's only useful from the static analysis point of view.

Having said that, I think the wrong behaviors reported at #42457 are more serious than the benefit for JET's static analysis.
We may be able to re-introduce N variables in the future once #39088 is resolved, and for the meanwhile, I'm okay to special case _isequal and such so that JET doesn't report errors there.

@martinholters
Copy link
Member Author

martinholters commented Oct 19, 2021

I'm not quite clear, does this PR have any advantage (besides the added tests) over reverting #40594?

If it's mainly about JET reporting false positives, then just getting rid of the type-asserts seems to cut it, i.e. just

--- a/base/tuple.jl
+++ b/base/tuple.jl
@@ -386,7 +386,7 @@ function _isequal(t1::Tuple{Any,Vararg{Any}}, t2::Tuple{Any,Vararg{Any}})
     isequal(t1[1], t2[1]) || return false
     t1, t2 = tail(t1), tail(t2)
     # avoid dynamic dispatch by telling the compiler relational invariants
-    return isa(t1, Tuple{}) ? _isequal((), t2::Tuple{}) : _isequal(t1, t2::Tuple{Any,Vararg{Any}})
+    return isa(t1, Tuple{}) ? _isequal((), t2) : _isequal(t1, t2)
 end

makes that report_call happy. Is that preferable?

@aviatesk
Copy link
Member

aviatesk commented Oct 19, 2021

I'd like to keep a part of #40594, since the current dispatch space of _isequal (_isequal(::Tuple{}, ::Tuple{}) / _isequal(::Tuple{Any,Vararg{Any}}, ::Tuple{Any,Vararg{Any}})) is better than before (_isequal(::Tuple{}, ::Tuple{}) / _isequal(::Tuple{Any}, ::Tuple{Any}) / _isequal(::Tuple, ::Tuple)
) in terms of # of methods and type domain separation.

And yeah, I think we can just eliminate the type assertion. We can even do:

diff --git a/base/tuple.jl b/base/tuple.jl
index 52c8ae1e3f..9ee5df3ccf 100644
--- a/base/tuple.jl
+++ b/base/tuple.jl
@@ -384,9 +384,7 @@ isequal(t1::Tuple, t2::Tuple) = length(t1) == length(t2) && _isequal(t1, t2)
 _isequal(::Tuple{}, ::Tuple{}) = true
 function _isequal(t1::Tuple{Any,Vararg{Any}}, t2::Tuple{Any,Vararg{Any}})
     isequal(t1[1], t2[1]) || return false
-    t1, t2 = tail(t1), tail(t2)
-    # avoid dynamic dispatch by telling the compiler relational invariants
-    return isa(t1, Tuple{}) ? _isequal((), t2::Tuple{}) : _isequal(t1, t2::Tuple{Any,Vararg{Any}})
+    return _isequal(tail(t1), tail(t2))
 end
 function _isequal(t1::Any32, t2::Any32)
     for i = 1:length(t1)

(I just confirmed JET is happy with the diff above)

julia> using JET

julia> report_call((Any, Tuple{Any})) do t1, t2
           Base._isequal(t1, t2)
       end
═════ 1 possible error found ═════
┌ @ REPL[2]:2 Base._isequal(t1, t2)
│┌ @ tuple.jl:389 Core.typeassert(t2, Core.apply_type(Base.Tuple, Base.Any, Core.apply_type(Base.Vararg, Base.Any)))
││ invalid builtin function call: Core.typeassert(t2::Tuple{}, Core.apply_type(Base.Tuple, Base.Any, Core.apply_type(Base.Vararg, Base.Any)::Core.TypeofVararg)::Type{Tuple{Any, Vararg{Any}}})
│└────────────────


julia> @eval Base begin
           delete_method.(methods(_isequal, (Tuple, Tuple)));
           
           _isequal(::Tuple{}, ::Tuple{}) = true
           function _isequal(t1::Tuple{Any,Vararg{Any}}, t2::Tuple{Any,Vararg{Any}})
               isequal(t1[1], t2[1]) || return false
               return _isequal(tail(t1), tail(t2))
           end
       end
_isequal (generic function with 2 methods)

julia> report_call((Any, Tuple{Any})) do t1, t2
           Base._isequal(t1, t2)
       end
No errors !


julia> report_call((Any, Tuple{Any,Any})) do t1, t2
           Base._isequal(t1, t2)
       end
No errors !

@martinholters
Copy link
Member Author

martinholters commented Oct 19, 2021

Ah, that looks quite nice. I'll do that and also take another look at the other functions whether similar simplifications are possible.

...by removing unnecessary type checks/assertions. This basically
restores the function bodies to what they looked like prior to #40594
and only keeps the changed signatures (with the additional changes to
circumvent #39088..
@martinholters
Copy link
Member Author

Indeed, looks like we can just revert to the function bodies from before #40594. I now get

for T1 in [Any, Tuple{Any}, Tuple{Any,Any}, Tuple{Any,Any,Any}],
    T2 in (T1 == Any ? [Tuple{Any}, Tuple{Any,Any}, Tuple{Any,Any,Any}] : [Any])
    display(report_call((T1, T2)) do t1, t2
        Base._isequal(t1, t2)
        Base.IteratorsMD._isless(0, t1, t2)
        Base.IteratorsMD.__inc(t1, t2)
        Base.IteratorsMD.__dec(t1, t2)
    end)
end

to produce "No errors !" six times.

For reference, this would be the diff of the relevant parts compared to before #40594:

-isequal(t1::Tuple, t2::Tuple) = (length(t1) == length(t2)) && _isequal(t1, t2)
-_isequal(t1::Tuple{}, t2::Tuple{}) = true
-_isequal(t1::Tuple{Any}, t2::Tuple{Any}) = isequal(t1[1], t2[1])
-_isequal(t1::Tuple, t2::Tuple) = isequal(t1[1], t2[1]) && _isequal(tail(t1), tail(t2))
+isequal(t1::Tuple, t2::Tuple) = length(t1) == length(t2) && _isequal(t1, t2)
+_isequal(::Tuple{}, ::Tuple{}) = true
+function _isequal(t1::Tuple{Any,Vararg{Any}}, t2::Tuple{Any,Vararg{Any}})
+    return isequal(t1[1], t2[1]) && _isequal(tail(t1), tail(t2))
+end
-    @inline function _isless(ret, I1::NTuple{N,Int}, I2::NTuple{N,Int}) where N
-        newret = ifelse(ret==0, icmp(I1[N], I2[N]), ret)
-        _isless(newret, Base.front(I1), Base.front(I2))
+    @inline function _isless(ret, I1::Tuple{Int,Vararg{Int}}, I2::Tuple{Int,Vararg{Int}})
+        newret = ifelse(ret==0, icmp(last(I1), last(I2)), ret)
+        return _isless(newret, Base.front(I1), Base.front(I2))
     end
     __inc(::Tuple{}, ::Tuple{}) = false, ()
-    @inline function __inc(state::Tuple{Int}, indices::Tuple{<:OrdinalRange})
+    @inline function __inc(state::Tuple{Int}, indices::Tuple{OrdinalRangeInt})
         rng = indices[1]
         I = state[1] + step(rng)
         valid = __is_valid_range(I, rng) && state[1] != last(rng)
         return valid, (I, )
     end
-    @inline function __inc(state, indices)
+    @inline function __inc(state::Tuple{Int,Int,Vararg{Int}}, indices::Tuple{OrdinalRangeInt,OrdinalRangeInt,Vararg{OrdinalRangeInt}})
         rng = indices[1]
         I = state[1] + step(rng)
         if __is_valid_range(I, rng) && state[1] != last(rng)
     @inline __dec(::Tuple{}, ::Tuple{}) = false, ()
-    @inline function __dec(state::Tuple{Int}, indices::Tuple{<:OrdinalRange})
+    @inline function __dec(state::Tuple{Int}, indices::Tuple{OrdinalRangeInt})
         rng = indices[1]
         I = state[1] - step(rng)
         valid = __is_valid_range(I, rng) && state[1] != first(rng)
         return valid, (I,)
     end
-
-    @inline function __dec(state, indices)
+    @inline function __dec(state::Tuple{Int,Int,Vararg{Int}}, indices::Tuple{OrdinalRangeInt,OrdinalRangeInt,Vararg{OrdinalRangeInt}})
         rng = indices[1]
         I = state[1] - step(rng)
         if __is_valid_range(I, rng) && state[1] != first(rng)

Copy link
Member

@aviatesk aviatesk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, the method bodies seem much cleaner than before, and it's also nice that we can keep method signatures introduced in #40594 !
Thanks for working on this :)

@KristofferC KristofferC merged commit df81a0d into master Oct 19, 2021
@KristofferC KristofferC deleted the mh/fix-42457 branch October 19, 2021 12:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Possibly incorrect inference through isequal
6 participants