diff --git a/NEWS.md b/NEWS.md index 879f68c47bb6a..0c24192c3a8da 100644 --- a/NEWS.md +++ b/NEWS.md @@ -17,6 +17,8 @@ is considered a bug fix ([#47102]) - The `hash` algorithm and its values have changed. Most `hash` specializations will remain correct and require no action. Types that reimplement the core hashing logic independently, such as some third-party string packages do, may require a migration to the new algorithm. ([#57509]) + - A `promote_type` invocation used to recur without bound in some cases while trying to get `promote_rule` to converge. There's now a fixed recursion depth limit to prevent infinite recursion, after which `promote_type` gives up, throwing `ArgumentError`. A subset of cases that would've caused infinite recursion before is caught even before reaching the depth limit. The depth limit is expected to be high enough for there to be no breakage due to this change. NB: there is in actuality no recursion any more, it is emulated up to a shallow depth via metaprogramming. ([#57517]) + * Indexless `getindex` and `setindex!` (i.e. `A[]`) on `ReinterpretArray` now correctly throw a `BoundsError` when there is more than one element. ([#58814]) Compiler/Runtime improvements diff --git a/base/operators.jl b/base/operators.jl index 51729b852070d..84ed22a74ef00 100644 --- a/base/operators.jl +++ b/base/operators.jl @@ -292,7 +292,7 @@ isunordered(x) = false isunordered(x::AbstractFloat) = isnan(x) isunordered(x::Missing) = true -==(T::Type, S::Type) = (@_total_meta; ccall(:jl_types_equal, Cint, (Any, Any), T, S) != 0) +==(T::Type, S::Type) = _types_are_equal(T, S) !=(T::Type, S::Type) = (@_total_meta; !(T == S)) ==(T::TypeVar, S::Type) = false ==(T::Type, S::TypeVar) = false diff --git a/base/promotion.jl b/base/promotion.jl index f935c546915be..cf6fe2b8e53c5 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -258,6 +258,17 @@ function tailjoin(A::SimpleVector, i::Int) return t end +## type equality + +function _types_are_equal(A::Type, B::Type) + @_total_meta + ccall(:jl_types_equal, Cint, (Any, Any), A, B) !== Cint(0) +end + +function _type_is_bottom(X::Type) + X === Bottom +end + ## promotion mechanism ## """ @@ -307,19 +318,149 @@ promote_type(T) = T promote_type(T, S, U) = (@inline; promote_type(promote_type(T, S), U)) promote_type(T, S, U, V...) = (@inline; afoldl(promote_type, promote_type(T, S, U), V...)) -promote_type(::Type{Bottom}, ::Type{Bottom}) = Bottom -promote_type(::Type{T}, ::Type{T}) where {T} = T -promote_type(::Type{T}, ::Type{Bottom}) where {T} = T -promote_type(::Type{Bottom}, ::Type{T}) where {T} = T +""" + TypePromotionError <: Exception + +Exception type thrown by [`promote_type`](@ref) when giving up for either of two reasons: + +* Because bugs in the [`promote_rule`](@ref) logic which would have caused infinite recursion were detected. + +* Because a threshold on the recursion depth was reached. + +Check for bugs in the [`promote_rule`](@ref) logic if this exception gets thrown. +""" +struct TypePromotionError <: Exception + T_initial::Type + S_initial::Type + T::Type + S::Type + ts::Type + st::Type + threshold_reached::Bool +end + +function showerror(io::IO, e::TypePromotionError) + print(io, "TypePromotionError: `promote_type(T, S)` failed: ") + + desc = if e.threshold_reached + "giving up because the recursion depth limit was reached: check for faulty/conflicting/missing `promote_rule` methods\n" + else + "detected unbounded recursion caused by faulty `promote_rule` logic\n" + end + + print(io, desc) + + print(io, " initial `T`: ") + show(io, e.T_initial) + print(io, '\n') + + print(io, " initial `S`: ") + show(io, e.S_initial) + print(io, '\n') + + print(io, " next-to-last `T`: ") + show(io, e.T) + print(io, '\n') + + print(io, " next-to-last `S`: ") + show(io, e.S) + print(io, '\n') + + print(io, " last `T`: ") + show(io, e.ts) + print(io, '\n') + + print(io, " last `S`: ") + show(io, e.st) + print(io, '\n') + + nothing +end + +function _promote_type_binary_err_giving_up(@nospecialize(T_initial::Type), @nospecialize(S_initial::Type), @nospecialize(T::Type), @nospecialize(S::Type), @nospecialize(ts::Type), @nospecialize(st::Type)) + @noinline + @_nospecializeinfer_meta + throw(TypePromotionError(T_initial, S_initial, T, S, ts, st, true)) +end +function _promote_type_binary_err_detected_infinite_recursion(@nospecialize(T_initial::Type), @nospecialize(S_initial::Type), @nospecialize(T::Type), @nospecialize(S::Type), @nospecialize(ts::Type), @nospecialize(st::Type)) + @noinline + @_nospecializeinfer_meta + throw(TypePromotionError(T_initial, S_initial, T, S, ts, st, false)) +end + +function _promote_type_binary_detect_loop(T::Type, S::Type, A::Type, B::Type) + onesided(T::Type, S::Type, A::Type, B::Type) = _types_are_equal(T, A) && _types_are_equal(S, B) + onesided(T, S, A, B) || onesided(T, S, B, A) +end + +macro _promote_type_binary_step1() + e = quote + # Try promote_rule in both orders. + ts = promote_rule(T, S) + st = promote_rule(S, T) + # If no promote_rule is defined, both directions give Bottom. In that + # case use typejoin on the original types instead. + st_is_bottom = _type_is_bottom(st) + ts_is_bottom = _type_is_bottom(ts) + if st_is_bottom && ts_is_bottom + return typejoin(T, S) + end + if ts_is_bottom + return st + end + if st_is_bottom || _types_are_equal(st, ts) + return ts + end + if _promote_type_binary_detect_loop(T, S, ts, st) + # This is not strictly necessary, as we already limit the recursion depth, but + # makes for nicer UX. + _promote_type_binary_err_detected_infinite_recursion(T_initial, S_initial, T, S, ts, st) + end + end + esc(e) +end + +macro _promote_type_binary_step2() + e = quote + T = ts + S = st + end + esc(e) +end + +macro _promote_type_binary_step() + e = quote + @_promote_type_binary_step1 + @_promote_type_binary_step2 + end + esc(e) +end + +function _promote_type_binary(::Type{T_initial}, ::Type{S_initial}) where {T_initial, S_initial} + T = T_initial + S = S_initial + + @_promote_type_binary_step + @_promote_type_binary_step + @_promote_type_binary_step + @_promote_type_binary_step + @_promote_type_binary_step + @_promote_type_binary_step + @_promote_type_binary_step + + @_promote_type_binary_step1 + + _promote_type_binary_err_giving_up(T_initial, S_initial, T, S, ts, st) +end function promote_type(::Type{T}, ::Type{S}) where {T,S} - @inline - # Try promote_rule in both orders. Typically only one is defined, - # and there is a fallback returning Bottom below, so the common case is - # promote_type(T, S) => - # promote_result(T, S, result, Bottom) => - # typejoin(result, Bottom) => result - promote_result(T, S, promote_rule(T,S), promote_rule(S,T)) + if _type_is_bottom(T) + return S + end + if _type_is_bottom(S) || _types_are_equal(S, T) + return T + end + _promote_type_binary(T, S) end """ @@ -339,11 +480,6 @@ promote_rule(::Type{Bottom}, ::Type{Bottom}, slurp...) = Bottom # not strictly n promote_rule(::Type{Bottom}, ::Type{T}, slurp...) where {T} = T promote_rule(::Type{T}, ::Type{Bottom}, slurp...) where {T} = T -promote_result(::Type,::Type,::Type{T},::Type{S}) where {T,S} = (@inline; promote_type(T,S)) -# If no promote_rule is defined, both directions give Bottom. In that -# case use typejoin on the original types instead. -promote_result(::Type{T},::Type{S},::Type{Bottom},::Type{Bottom}) where {T,S} = (@inline; typejoin(T, S)) - """ promote(xs...) diff --git a/test/core.jl b/test/core.jl index a824c0abb674a..222b5a53aad7f 100644 --- a/test/core.jl +++ b/test/core.jl @@ -2019,6 +2019,29 @@ g4731() = f4731() @test f4731() == "" @test g4731() == "" +@testset "issue #13193" begin + struct Issue13193_SIQuantity{T<:Number} <: Number end + Base.promote_rule(::Type{Issue13193_SIQuantity{T}}, ::Type{Issue13193_SIQuantity{S}}) where {T, S} = Issue13193_SIQuantity{promote_type(T,S)} + Base.promote_rule(::Type{Issue13193_SIQuantity{T}}, ::Type{S}) where {T, S<:Number} = Issue13193_SIQuantity{promote_type(T,S)} + struct Issue13193_Interval{T<:Number} <: Number end + Base.promote_rule(::Type{Issue13193_Interval{T}}, ::Type{Issue13193_Interval{S}}) where {T, S} = Issue13193_Interval{promote_type(T,S)} + Base.promote_rule(::Type{Issue13193_Interval{T}}, ::Type{S}) where {T, S<:Number} = Issue13193_Interval{promote_type(T,S)} + @test_throws Base.TypePromotionError promote_type(Issue13193_Interval{Int}, Issue13193_SIQuantity{Int}) + @test_throws Base.TypePromotionError promote_type(Issue13193_SIQuantity{Int}, Issue13193_Interval{Int}) + @test Base.Compiler.is_foldable(Base.infer_effects(promote_type, Tuple{Type{Issue13193_Interval{Int}}, Type{Issue13193_SIQuantity{Int}}})) + @test Base.Compiler.is_foldable(Base.infer_effects(promote_type, Tuple{Type{Issue13193_SIQuantity{Int}}, Type{Issue13193_Interval{Int}}})) +end +@testset "straightforward conflict in `promote_rule` definitions" begin + struct ConflictingPromoteRuleDefinitionsA end + struct ConflictingPromoteRuleDefinitionsB end + Base.promote_rule(::Type{ConflictingPromoteRuleDefinitionsA}, ::Type{ConflictingPromoteRuleDefinitionsB}) = ConflictingPromoteRuleDefinitionsA + Base.promote_rule(::Type{ConflictingPromoteRuleDefinitionsB}, ::Type{ConflictingPromoteRuleDefinitionsA}) = ConflictingPromoteRuleDefinitionsB + @test_throws Base.TypePromotionError promote_type(ConflictingPromoteRuleDefinitionsA, ConflictingPromoteRuleDefinitionsB) + @test_throws Base.TypePromotionError promote_type(ConflictingPromoteRuleDefinitionsB, ConflictingPromoteRuleDefinitionsA) + @test Base.Compiler.is_foldable(Base.infer_effects(promote_type, Tuple{Type{ConflictingPromoteRuleDefinitionsA}, Type{ConflictingPromoteRuleDefinitionsB}})) + @test Base.Compiler.is_foldable(Base.infer_effects(promote_type, Tuple{Type{ConflictingPromoteRuleDefinitionsB}, Type{ConflictingPromoteRuleDefinitionsA}})) +end + # issue #4675 f4675(x::StridedArray...) = 1 f4675(x::StridedArray{T}...) where {T} = 2