From 4e935a972b6518ba0770a896e556c36cba1bfbc6 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Mon, 24 Feb 2025 12:33:17 +0100 Subject: [PATCH 01/51] prevent stack overflow in 2-arg `promote_type` Don't use recursion, delete the `promote_result` function. Use loop known to terminate, because of a hardcoded limit on the iteration count. Closes #57507, this PR encompasses the fixes from that PR and is more comprehensive. Fixes #13193 --- base/promotion.jl | 62 ++++++++++++++++++++++++++++++++++------------- test/core.jl | 31 ++++++++++++++++++++++++ 2 files changed, 76 insertions(+), 17 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index f935c546915be..80f90ba91ef76 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -307,19 +307,52 @@ 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 - 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)) + @_terminates_locally_meta + normalized_type(::Type{Typ}) where {Typ} = Typ + types_are_equal(::Type, ::Type) = false + types_are_equal(::Type{Typ}, ::Type{Typ}) where {Typ} = true + is_bottom(::Type) = false + is_bottom(::Type{Bottom}) = true + function throw_conflicting_promote_rules((@nospecialize i1::Type), (@nospecialize i2::Type), (@nospecialize left::Type), (@nospecialize right::Type)) + @noinline + s = LazyString("`promote_type(", i1, ", ", i2, ")` failed, there are conflicting `promote_rule` definitions for types ", left, ", ", right) + throw(ArgumentError(s)) + end + function throw_gave_up((@nospecialize i1::Type), (@nospecialize i2::Type), (@nospecialize left::Type), (@nospecialize right::Type)) + @noinline + s = LazyString("`promote_type(", i1, ", ", i2, ")` failed, ended up with (", left, ", ", right, "), check for faulty `promote_rule` methods") + throw(ArgumentError(s)) + end + left = T + right = S + for _ ∈ 1:1000 # guarantee local termination + if types_are_equal(left, right) || is_bottom(left) || is_bottom(right) + break + end + # Try `promote_rule` in both orders. + a = normalized_type(promote_rule(left, right)) + b = normalized_type(promote_rule(right, left)) + loop_is_detected_1 = types_are_equal(left, a) && types_are_equal(right, b) + loop_is_detected_2 = types_are_equal(left, b) && types_are_equal(right, a) + if loop_is_detected_1 || loop_is_detected_2 + throw_conflicting_promote_rules(T, S, left, right) + end + if is_bottom(a) && is_bottom(b) + # If no `promote_rule` is defined, both directions give `Bottom`. In that + # case use `typejoin` on the original types. + return typejoin(left, right) + end + left = a + right = b + end + if types_are_equal(left, right) || is_bottom(left) + right + elseif is_bottom(right) + left + else + throw_gave_up(T, S, left, right) + end end """ @@ -339,11 +372,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 c84d68bafece9..bf159100468b5 100644 --- a/test/core.jl +++ b/test/core.jl @@ -2019,6 +2019,37 @@ g4731() = f4731() @test f4731() == "" @test g4731() == "" +@testset "type promotion" begin + @testset "conflicting promote_rule error, PR #57507" begin + struct PR57507A end + struct PR57507B end + struct PR57507C end + @testset "error with conflicting promote_rules" begin + Base.promote_rule(::Type{PR57507A}, ::Type{PR57507B}) = PR57507A + Base.promote_rule(::Type{PR57507B}, ::Type{PR57507A}) = PR57507B + @test_throws ArgumentError promote_type(PR57507A, PR57507B) + @test_throws ArgumentError promote_type(PR57507B, PR57507A) + end + @testset "unambiguous cases" begin + @test PR57507A === @inferred promote_type(PR57507A, PR57507A) + @test PR57507B === @inferred promote_type(PR57507B, PR57507B) + Base.promote_rule(::Type{PR57507C}, ::Type{PR57507A}) = PR57507C + Base.promote_rule(::Type{PR57507B}, ::Type{PR57507C}) = PR57507C + @test PR57507C === @inferred promote_type(PR57507A, PR57507C) + @test PR57507C === @inferred promote_type(PR57507C, PR57507B) + end + end + @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 ArgumentError promote_type(Issue13193_Interval{Int}, Issue13193_SIQuantity{Int}) + end +end + # issue #4675 f4675(x::StridedArray...) = 1 f4675(x::StridedArray{T}...) where {T} = 2 From 4ae0df1019b11a297fa95b17fcb28109382e1c17 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Mon, 24 Feb 2025 22:56:31 +0100 Subject: [PATCH 02/51] reduce PR scope, use recursion instead of looping for better inference --- base/promotion.jl | 81 +++++++++++++++++++++-------------------------- test/core.jl | 37 +++++----------------- 2 files changed, 44 insertions(+), 74 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 80f90ba91ef76..fb9d3e6f97513 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -307,52 +307,38 @@ 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...)) +function _promote_type_binary(::Type, ::Type, ::Tuple{}) + @noinline + s = "`promote_type`: recursion depth limit reached, giving up; check for faulty/conflicting/missing `promote_rule` methods" + throw(ArgumentError(s)) +end +function _promote_type_binary(::Type{Bottom}, ::Type{Bottom}, ::Tuple{Nothing,Vararg{Nothing}}) + Bottom +end +function _promote_type_binary(::Type{T}, ::Type{T}, ::Tuple{Nothing,Vararg{Nothing}}) where {T} + T +end +function _promote_type_binary(::Type{T}, ::Type{Bottom}, ::Tuple{Nothing,Vararg{Nothing}}) where {T} + T +end +function _promote_type_binary(::Type{Bottom}, ::Type{T}, ::Tuple{Nothing,Vararg{Nothing}}) where {T} + T +end +function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple{Nothing,Vararg{Nothing}}) where {T,S} + # Try promote_rule in both orders. + promote_result(T, S, promote_rule(T,S), promote_rule(S,T), recursion_depth_limit) +end + +const _promote_type_binary_recursion_depth_limit = ((nothing for _ in 1:28)...,) # recursion depth limit to prevent stack overflow + function promote_type(::Type{T}, ::Type{S}) where {T,S} - @_terminates_locally_meta - normalized_type(::Type{Typ}) where {Typ} = Typ - types_are_equal(::Type, ::Type) = false - types_are_equal(::Type{Typ}, ::Type{Typ}) where {Typ} = true - is_bottom(::Type) = false - is_bottom(::Type{Bottom}) = true - function throw_conflicting_promote_rules((@nospecialize i1::Type), (@nospecialize i2::Type), (@nospecialize left::Type), (@nospecialize right::Type)) - @noinline - s = LazyString("`promote_type(", i1, ", ", i2, ")` failed, there are conflicting `promote_rule` definitions for types ", left, ", ", right) - throw(ArgumentError(s)) - end - function throw_gave_up((@nospecialize i1::Type), (@nospecialize i2::Type), (@nospecialize left::Type), (@nospecialize right::Type)) - @noinline - s = LazyString("`promote_type(", i1, ", ", i2, ")` failed, ended up with (", left, ", ", right, "), check for faulty `promote_rule` methods") - throw(ArgumentError(s)) - end - left = T - right = S - for _ ∈ 1:1000 # guarantee local termination - if types_are_equal(left, right) || is_bottom(left) || is_bottom(right) - break - end - # Try `promote_rule` in both orders. - a = normalized_type(promote_rule(left, right)) - b = normalized_type(promote_rule(right, left)) - loop_is_detected_1 = types_are_equal(left, a) && types_are_equal(right, b) - loop_is_detected_2 = types_are_equal(left, b) && types_are_equal(right, a) - if loop_is_detected_1 || loop_is_detected_2 - throw_conflicting_promote_rules(T, S, left, right) - end - if is_bottom(a) && is_bottom(b) - # If no `promote_rule` is defined, both directions give `Bottom`. In that - # case use `typejoin` on the original types. - return typejoin(left, right) - end - left = a - right = b - end - if types_are_equal(left, right) || is_bottom(left) - right - elseif is_bottom(right) - left - else - throw_gave_up(T, S, left, right) - end + @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_type_binary(T, S, _promote_type_binary_recursion_depth_limit) end """ @@ -372,6 +358,11 @@ 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},l::Tuple{Vararg{Nothing}}) where {T,S} = (@inline; _promote_type_binary(T,S,l)) +# 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},::Tuple{Vararg{Nothing}}) where {T,S} = (@inline; typejoin(T, S)) + """ promote(xs...) diff --git a/test/core.jl b/test/core.jl index bf159100468b5..95af16a1d0bd3 100644 --- a/test/core.jl +++ b/test/core.jl @@ -2019,35 +2019,14 @@ g4731() = f4731() @test f4731() == "" @test g4731() == "" -@testset "type promotion" begin - @testset "conflicting promote_rule error, PR #57507" begin - struct PR57507A end - struct PR57507B end - struct PR57507C end - @testset "error with conflicting promote_rules" begin - Base.promote_rule(::Type{PR57507A}, ::Type{PR57507B}) = PR57507A - Base.promote_rule(::Type{PR57507B}, ::Type{PR57507A}) = PR57507B - @test_throws ArgumentError promote_type(PR57507A, PR57507B) - @test_throws ArgumentError promote_type(PR57507B, PR57507A) - end - @testset "unambiguous cases" begin - @test PR57507A === @inferred promote_type(PR57507A, PR57507A) - @test PR57507B === @inferred promote_type(PR57507B, PR57507B) - Base.promote_rule(::Type{PR57507C}, ::Type{PR57507A}) = PR57507C - Base.promote_rule(::Type{PR57507B}, ::Type{PR57507C}) = PR57507C - @test PR57507C === @inferred promote_type(PR57507A, PR57507C) - @test PR57507C === @inferred promote_type(PR57507C, PR57507B) - end - end - @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 ArgumentError promote_type(Issue13193_Interval{Int}, Issue13193_SIQuantity{Int}) - end +@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 ArgumentError promote_type(Issue13193_Interval{Int}, Issue13193_SIQuantity{Int}) end # issue #4675 From 4a64b6949ef108cb74238ecf2a4b5716788ae548 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Mon, 24 Feb 2025 23:04:17 +0100 Subject: [PATCH 03/51] fix --- base/promotion.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index fb9d3e6f97513..f7e4de8d993e8 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -325,8 +325,9 @@ function _promote_type_binary(::Type{Bottom}, ::Type{T}, ::Tuple{Nothing,Vararg{ T end function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple{Nothing,Vararg{Nothing}}) where {T,S} + l = tail(recursion_depth_limit) # Try promote_rule in both orders. - promote_result(T, S, promote_rule(T,S), promote_rule(S,T), recursion_depth_limit) + promote_result(T, S, promote_rule(T,S), promote_rule(S,T), l) end const _promote_type_binary_recursion_depth_limit = ((nothing for _ in 1:28)...,) # recursion depth limit to prevent stack overflow From b3c5fdb5d94d7bdee0451758d1e2e838d1ca0bdc Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Mon, 24 Feb 2025 23:10:54 +0100 Subject: [PATCH 04/51] fix bootstrap --- base/promotion.jl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index f7e4de8d993e8..53192137fbfc0 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -330,7 +330,13 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple promote_result(T, S, promote_rule(T,S), promote_rule(S,T), l) end -const _promote_type_binary_recursion_depth_limit = ((nothing for _ in 1:28)...,) # recursion depth limit to prevent stack overflow +const _promote_type_binary_recursion_depth_limit = let n = nothing # recursion depth limit to prevent stack overflow + ( + n, n, n, n, n, n, n, n, + n, n, n, n, n, n, n, n, + n, n, n, n, n, n, n, n, + ) +end function promote_type(::Type{T}, ::Type{S}) where {T,S} @inline From 9ce7c8aca1186d014275f7f00a8aa2ff9e751315 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Mon, 24 Feb 2025 23:29:49 +0100 Subject: [PATCH 05/51] nicer tuple definition, try if this fixes bootstrap --- base/promotion.jl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 53192137fbfc0..51ff9f866d0ce 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -331,11 +331,8 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple end const _promote_type_binary_recursion_depth_limit = let n = nothing # recursion depth limit to prevent stack overflow - ( - n, n, n, n, n, n, n, n, - n, n, n, n, n, n, n, n, - n, n, n, n, n, n, n, n, - ) + f = (n, n, n, n) + (f..., f...) end function promote_type(::Type{T}, ::Type{S}) where {T,S} From fffa54d092000531b50ccc05314c72928d3cb1f5 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Mon, 24 Feb 2025 23:39:33 +0100 Subject: [PATCH 06/51] try increasing the limit, hopefully without breaking bootstrap again --- base/promotion.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index 51ff9f866d0ce..9ae31a859f62d 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -332,7 +332,7 @@ end const _promote_type_binary_recursion_depth_limit = let n = nothing # recursion depth limit to prevent stack overflow f = (n, n, n, n) - (f..., f...) + (f..., f..., f..., f..., f...) end function promote_type(::Type{T}, ::Type{S}) where {T,S} From abd6288fa96479ca75c9f453b50929157c20595b Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Mon, 24 Feb 2025 23:47:03 +0100 Subject: [PATCH 07/51] decrease limit, hopefully fixing the bootstrap --- base/promotion.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 9ae31a859f62d..6316fe3226466 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -331,8 +331,9 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple end const _promote_type_binary_recursion_depth_limit = let n = nothing # recursion depth limit to prevent stack overflow - f = (n, n, n, n) - (f..., f..., f..., f..., f...) + n2 = (n, n) + n4 = (n2..., n2...) + (n4..., n4..., n2...) end function promote_type(::Type{T}, ::Type{S}) where {T,S} From 0b3be6e92b5ed50d5328b06fda3fba8d89abf1b7 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 18:22:46 +0200 Subject: [PATCH 08/51] make the exception a constant binding --- base/promotion.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 6316fe3226466..73d0496e05863 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -307,10 +307,13 @@ 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...)) +const _promote_type_binary_recursion_depth_limit_exception = let + s = "`promote_type`: recursion depth limit reached, giving up; check for faulty/conflicting/missing `promote_rule` methods" + ArgumentError(s) +end function _promote_type_binary(::Type, ::Type, ::Tuple{}) @noinline - s = "`promote_type`: recursion depth limit reached, giving up; check for faulty/conflicting/missing `promote_rule` methods" - throw(ArgumentError(s)) + throw(_promote_type_binary_recursion_depth_limit_exception) end function _promote_type_binary(::Type{Bottom}, ::Type{Bottom}, ::Tuple{Nothing,Vararg{Nothing}}) Bottom From 062b6d3f647d0d5dd4819100e59236e6dfd79ece Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 18:30:17 +0200 Subject: [PATCH 09/51] tiny style improvement 1 --- base/promotion.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 73d0496e05863..185f5404c832a 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -333,8 +333,8 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple promote_result(T, S, promote_rule(T,S), promote_rule(S,T), l) end -const _promote_type_binary_recursion_depth_limit = let n = nothing # recursion depth limit to prevent stack overflow - n2 = (n, n) +const _promote_type_binary_recursion_depth_limit = let # recursion depth limit to prevent stack overflow + n2 = (nothing, nothing) n4 = (n2..., n2...) (n4..., n4..., n2...) end From 31be3d01d8e8b4ab1a3a8958ffc542453a076adf Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 18:30:35 +0200 Subject: [PATCH 10/51] style improvement 2 --- base/promotion.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index 185f5404c832a..55d8e2c6aa346 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -336,7 +336,8 @@ end const _promote_type_binary_recursion_depth_limit = let # recursion depth limit to prevent stack overflow n2 = (nothing, nothing) n4 = (n2..., n2...) - (n4..., n4..., n2...) + n8 = (n4..., n4...) + (n8..., n2...) end function promote_type(::Type{T}, ::Type{S}) where {T,S} From 414e67a21863763b15d4d2c29444bda07557aac0 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 18:34:32 +0200 Subject: [PATCH 11/51] use a doc string instead of a comment --- base/promotion.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index 55d8e2c6aa346..61b2a81e82e37 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -333,7 +333,12 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple promote_result(T, S, promote_rule(T,S), promote_rule(S,T), l) end -const _promote_type_binary_recursion_depth_limit = let # recursion depth limit to prevent stack overflow +""" + _promote_type_binary_recursion_depth_limit::Tuple{Vararg{Nothing}} + +Recursion depth limit for `_promote_type_binary`, to prevent stack overflow. +""" +const _promote_type_binary_recursion_depth_limit = let n2 = (nothing, nothing) n4 = (n2..., n2...) n8 = (n4..., n4...) From 91cb213119a41862a6504e3499088408c18d0bb7 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 20:49:39 +0200 Subject: [PATCH 12/51] merge all `_promote_type_binary` methods into a single one --- base/promotion.jl | 28 +++++++++++----------------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 61b2a81e82e37..a71ee5d5b9567 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -311,23 +311,17 @@ const _promote_type_binary_recursion_depth_limit_exception = let s = "`promote_type`: recursion depth limit reached, giving up; check for faulty/conflicting/missing `promote_rule` methods" ArgumentError(s) end -function _promote_type_binary(::Type, ::Type, ::Tuple{}) - @noinline - throw(_promote_type_binary_recursion_depth_limit_exception) -end -function _promote_type_binary(::Type{Bottom}, ::Type{Bottom}, ::Tuple{Nothing,Vararg{Nothing}}) - Bottom -end -function _promote_type_binary(::Type{T}, ::Type{T}, ::Tuple{Nothing,Vararg{Nothing}}) where {T} - T -end -function _promote_type_binary(::Type{T}, ::Type{Bottom}, ::Tuple{Nothing,Vararg{Nothing}}) where {T} - T -end -function _promote_type_binary(::Type{Bottom}, ::Type{T}, ::Tuple{Nothing,Vararg{Nothing}}) where {T} - T -end -function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple{Nothing,Vararg{Nothing}}) where {T,S} +function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple{Vararg{Nothing}}) where {T,S} + err() = throw(_promote_type_binary_recursion_depth_limit_exception) + if recursion_depth_limit === () + @noinline err() + end + if T <: Bottom + return S + end + if S <: Bottom + return T + end l = tail(recursion_depth_limit) # Try promote_rule in both orders. promote_result(T, S, promote_rule(T,S), promote_rule(S,T), l) From 33844864564d290296ba5d8369d2f203abcde66a Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 20:57:30 +0200 Subject: [PATCH 13/51] `_promote_type_binary`: style: use more local variables --- base/promotion.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index a71ee5d5b9567..c3d9c639fe2bd 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -324,7 +324,9 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple end l = tail(recursion_depth_limit) # Try promote_rule in both orders. - promote_result(T, S, promote_rule(T,S), promote_rule(S,T), l) + st = promote_rule(S, T) + ts = promote_rule(T, S) + promote_result(T, S, ts, st, l) end """ From 6cde4683fc5815365c887690681b6be31ff3e3fe Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 21:06:53 +0200 Subject: [PATCH 14/51] inline `promote_result` into `_promote_type_binary` --- base/promotion.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index c3d9c639fe2bd..20777d905e496 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -319,14 +319,19 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple if T <: Bottom return S end - if S <: Bottom + if (S <: Bottom) || ((S <: T) && (T <: S)) return T end l = tail(recursion_depth_limit) # Try promote_rule in both orders. st = promote_rule(S, T) ts = promote_rule(T, S) - promote_result(T, S, ts, st, l) + # If no promote_rule is defined, both directions give Bottom. In that + # case use typejoin on the original types instead. + if (st <: Bottom) && (ts <: Bottom) + typejoin(T, S) + end + _promote_type_binary(st, ts, l) end """ @@ -368,11 +373,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},l::Tuple{Vararg{Nothing}}) where {T,S} = (@inline; _promote_type_binary(T,S,l)) -# 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},::Tuple{Vararg{Nothing}}) where {T,S} = (@inline; typejoin(T, S)) - """ promote(xs...) From 86da78c631e20172580d7828f6dde88180555ab6 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 21:09:21 +0200 Subject: [PATCH 15/51] normalize the output of `promote_rule` before using it --- base/promotion.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 20777d905e496..9f3bb1e0ef457 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -313,6 +313,7 @@ const _promote_type_binary_recursion_depth_limit_exception = let end function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple{Vararg{Nothing}}) where {T,S} err() = throw(_promote_type_binary_recursion_depth_limit_exception) + normalize_type(::Type{X}) where {X} = X if recursion_depth_limit === () @noinline err() end @@ -324,8 +325,8 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple end l = tail(recursion_depth_limit) # Try promote_rule in both orders. - st = promote_rule(S, T) - ts = promote_rule(T, S) + st = normalize_type(promote_rule(S, T)) + ts = normalize_type(promote_rule(T, S)) # If no promote_rule is defined, both directions give Bottom. In that # case use typejoin on the original types instead. if (st <: Bottom) && (ts <: Bottom) From d4ad8689d6f32be8d801df1ddc694f25eecd250a Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 21:29:08 +0200 Subject: [PATCH 16/51] fix bootstrap: macro `noinline` --- base/promotion.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 9f3bb1e0ef457..e5f31fe7d77da 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -312,10 +312,13 @@ const _promote_type_binary_recursion_depth_limit_exception = let ArgumentError(s) end function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple{Vararg{Nothing}}) where {T,S} - err() = throw(_promote_type_binary_recursion_depth_limit_exception) + function err() + @noinline + throw(_promote_type_binary_recursion_depth_limit_exception) + end normalize_type(::Type{X}) where {X} = X if recursion_depth_limit === () - @noinline err() + err() end if T <: Bottom return S From 287f16fd7383972e3c5108d78a5e69751f07af0d Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 22:04:36 +0200 Subject: [PATCH 17/51] order like before --- base/promotion.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index e5f31fe7d77da..2702103019af8 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -335,7 +335,7 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple if (st <: Bottom) && (ts <: Bottom) typejoin(T, S) end - _promote_type_binary(st, ts, l) + _promote_type_binary(ts, st, l) end """ From 279c7a042b4f8b208931420e355a6f6cafc08940 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 22:13:06 +0200 Subject: [PATCH 18/51] fix --- base/promotion.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index 2702103019af8..f1cb99aa25cb6 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -334,8 +334,9 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple # case use typejoin on the original types instead. if (st <: Bottom) && (ts <: Bottom) typejoin(T, S) + else + _promote_type_binary(ts, st, l) end - _promote_type_binary(ts, st, l) end """ From 66e3eb13b5b0d195c3950a2ebb9a9274f5e94f3b Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 22:40:37 +0200 Subject: [PATCH 19/51] also normalize the output of `typejoin` --- base/promotion.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index f1cb99aa25cb6..be198812001ad 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -333,7 +333,7 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple # If no promote_rule is defined, both directions give Bottom. In that # case use typejoin on the original types instead. if (st <: Bottom) && (ts <: Bottom) - typejoin(T, S) + normalize_type(typejoin(T, S)) else _promote_type_binary(ts, st, l) end From 31d2b46a7ba08a0528a2527070514e17dcb62f5f Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Tue, 3 Jun 2025 21:42:22 +0200 Subject: [PATCH 20/51] check recursion depth only after checking subtyping Don't throw unnecessarily. --- base/promotion.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index be198812001ad..cf72d16387aa3 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -317,15 +317,15 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple throw(_promote_type_binary_recursion_depth_limit_exception) end normalize_type(::Type{X}) where {X} = X - if recursion_depth_limit === () - err() - end if T <: Bottom return S end if (S <: Bottom) || ((S <: T) && (T <: S)) return T end + if recursion_depth_limit === () + err() + end l = tail(recursion_depth_limit) # Try promote_rule in both orders. st = normalize_type(promote_rule(S, T)) From c3e1cdcc222acea0e2aa112e817f664e1c9e79dc Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Wed, 4 Jun 2025 00:22:16 +0200 Subject: [PATCH 21/51] factor out subtyping checks into functions --- base/promotion.jl | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index cf72d16387aa3..94b5b6979d676 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -316,11 +316,16 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple @noinline throw(_promote_type_binary_recursion_depth_limit_exception) end + type_is_bottom(::Type{X}) where {X} = X === Bottom + function types_are_equal(::Type{A}, ::Type{B}) where {A,B} + @_total_meta + ccall(:jl_types_equal, Cint, (Any, Any), A, B) !== Cint(0) + end normalize_type(::Type{X}) where {X} = X - if T <: Bottom + if type_is_bottom(T) return S end - if (S <: Bottom) || ((S <: T) && (T <: S)) + if type_is_bottom(S) || types_are_equal(S, T) return T end if recursion_depth_limit === () @@ -332,7 +337,7 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple ts = normalize_type(promote_rule(T, S)) # If no promote_rule is defined, both directions give Bottom. In that # case use typejoin on the original types instead. - if (st <: Bottom) && (ts <: Bottom) + if type_is_bottom(st) && type_is_bottom(ts) normalize_type(typejoin(T, S)) else _promote_type_binary(ts, st, l) From b6d19e3d51144ed56b01c97314bf6e6be0e60d0e Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Wed, 4 Jun 2025 01:06:59 +0200 Subject: [PATCH 22/51] delete `inline` macro invocation and delete outdated comment --- base/promotion.jl | 6 ------ 1 file changed, 6 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 94b5b6979d676..ff083dc174923 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -357,12 +357,6 @@ const _promote_type_binary_recursion_depth_limit = let 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_type_binary(T, S, _promote_type_binary_recursion_depth_limit) end From bf40548f7622f2fa347c759fbea538a7af1b4d1f Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Wed, 4 Jun 2025 09:51:16 +0200 Subject: [PATCH 23/51] go back to a loop, avoid recursion, see if that's OK now --- base/promotion.jl | 88 ++++++++++++++++++++++------------------------- test/core.jl | 37 +++++++++++++++----- 2 files changed, 70 insertions(+), 55 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index ff083dc174923..6ffec4b3aa3c4 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -307,57 +307,51 @@ 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...)) -const _promote_type_binary_recursion_depth_limit_exception = let - s = "`promote_type`: recursion depth limit reached, giving up; check for faulty/conflicting/missing `promote_rule` methods" - ArgumentError(s) -end -function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple{Vararg{Nothing}}) where {T,S} - function err() +function promote_type(::Type{T}, ::Type{S}) where {T,S} + @_terminates_locally_meta + normalized_type(::Type{Typ}) where {Typ} = Typ + normalized_promote_rule(::Type{A}, ::Type{B}) where {A,B} = normalized_type(promote_rule(A, B)) + types_are_equal(::Type{A}, ::Type{B}) where {A,B} = (A <: B) && (B <: A) + is_bottom(::Type{Typ}) where {Typ} = Typ === Bottom + function throw_conflicting_promote_rules((@nospecialize i1::Type), (@nospecialize i2::Type), (@nospecialize left::Type), (@nospecialize right::Type)) @noinline - throw(_promote_type_binary_recursion_depth_limit_exception) - end - type_is_bottom(::Type{X}) where {X} = X === Bottom - function types_are_equal(::Type{A}, ::Type{B}) where {A,B} - @_total_meta - ccall(:jl_types_equal, Cint, (Any, Any), A, B) !== Cint(0) - end - normalize_type(::Type{X}) where {X} = X - if type_is_bottom(T) - return S - end - if type_is_bottom(S) || types_are_equal(S, T) - return T + @_nospecializeinfer_meta + s = LazyString("`promote_type(", i1, ", ", i2, ")` failed, there are conflicting `promote_rule` definitions for types ", left, ", ", right) + throw(ArgumentError(s)) end - if recursion_depth_limit === () - err() + function throw_gave_up((@nospecialize i1::Type), (@nospecialize i2::Type), (@nospecialize left::Type), (@nospecialize right::Type)) + @noinline + @_nospecializeinfer_meta + s = LazyString("`promote_type(", i1, ", ", i2, ")` failed, ended up with (", left, ", ", right, "), check for faulty/conflicting/missing `promote_rule` methods") + throw(ArgumentError(s)) end - l = tail(recursion_depth_limit) - # Try promote_rule in both orders. - st = normalize_type(promote_rule(S, T)) - ts = normalize_type(promote_rule(T, S)) - # If no promote_rule is defined, both directions give Bottom. In that - # case use typejoin on the original types instead. - if type_is_bottom(st) && type_is_bottom(ts) - normalize_type(typejoin(T, S)) - else - _promote_type_binary(ts, st, l) + left = T + right = S + for _ ∈ 1:50 # must be finite to guarantee local termination + if is_bottom(left) + return right + end + if is_bottom(right) || types_are_equal(left, right) + return left + end + # Try `promote_rule` in both orders. + a = normalized_promote_rule(left, right) + b = normalized_promote_rule(right, left) + if ( + (types_are_equal(left, a) && types_are_equal(right, b)) || + (types_are_equal(left, b) && types_are_equal(right, a)) + ) + throw_conflicting_promote_rules(T, S, left, right) + end + if is_bottom(a) && is_bottom(b) + # If no `promote_rule` is defined, both directions give `Bottom`. In that + # case use `typejoin` on the original types. + return normalized_type(typejoin(left, right)) + end + left = a + right = b end -end - -""" - _promote_type_binary_recursion_depth_limit::Tuple{Vararg{Nothing}} - -Recursion depth limit for `_promote_type_binary`, to prevent stack overflow. -""" -const _promote_type_binary_recursion_depth_limit = let - n2 = (nothing, nothing) - n4 = (n2..., n2...) - n8 = (n4..., n4...) - (n8..., n2...) -end - -function promote_type(::Type{T}, ::Type{S}) where {T,S} - _promote_type_binary(T, S, _promote_type_binary_recursion_depth_limit) + throw_gave_up(T, S, left, right) end """ diff --git a/test/core.jl b/test/core.jl index 95af16a1d0bd3..bf159100468b5 100644 --- a/test/core.jl +++ b/test/core.jl @@ -2019,14 +2019,35 @@ 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 ArgumentError promote_type(Issue13193_Interval{Int}, Issue13193_SIQuantity{Int}) +@testset "type promotion" begin + @testset "conflicting promote_rule error, PR #57507" begin + struct PR57507A end + struct PR57507B end + struct PR57507C end + @testset "error with conflicting promote_rules" begin + Base.promote_rule(::Type{PR57507A}, ::Type{PR57507B}) = PR57507A + Base.promote_rule(::Type{PR57507B}, ::Type{PR57507A}) = PR57507B + @test_throws ArgumentError promote_type(PR57507A, PR57507B) + @test_throws ArgumentError promote_type(PR57507B, PR57507A) + end + @testset "unambiguous cases" begin + @test PR57507A === @inferred promote_type(PR57507A, PR57507A) + @test PR57507B === @inferred promote_type(PR57507B, PR57507B) + Base.promote_rule(::Type{PR57507C}, ::Type{PR57507A}) = PR57507C + Base.promote_rule(::Type{PR57507B}, ::Type{PR57507C}) = PR57507C + @test PR57507C === @inferred promote_type(PR57507A, PR57507C) + @test PR57507C === @inferred promote_type(PR57507C, PR57507B) + end + end + @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 ArgumentError promote_type(Issue13193_Interval{Int}, Issue13193_SIQuantity{Int}) + end end # issue #4675 From 61873441a8e0e4dcac647281a078df239c4ae70c Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Wed, 4 Jun 2025 11:58:41 +0200 Subject: [PATCH 24/51] Revert "go back to a loop, avoid recursion, see if that's OK now" This reverts commit 66336e0197dd2028edc4391bbd6defc2c8e5e9b8. --- base/promotion.jl | 88 +++++++++++++++++++++++++---------------------- test/core.jl | 37 +++++--------------- 2 files changed, 55 insertions(+), 70 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 6ffec4b3aa3c4..ff083dc174923 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -307,51 +307,57 @@ 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...)) -function promote_type(::Type{T}, ::Type{S}) where {T,S} - @_terminates_locally_meta - normalized_type(::Type{Typ}) where {Typ} = Typ - normalized_promote_rule(::Type{A}, ::Type{B}) where {A,B} = normalized_type(promote_rule(A, B)) - types_are_equal(::Type{A}, ::Type{B}) where {A,B} = (A <: B) && (B <: A) - is_bottom(::Type{Typ}) where {Typ} = Typ === Bottom - function throw_conflicting_promote_rules((@nospecialize i1::Type), (@nospecialize i2::Type), (@nospecialize left::Type), (@nospecialize right::Type)) +const _promote_type_binary_recursion_depth_limit_exception = let + s = "`promote_type`: recursion depth limit reached, giving up; check for faulty/conflicting/missing `promote_rule` methods" + ArgumentError(s) +end +function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple{Vararg{Nothing}}) where {T,S} + function err() @noinline - @_nospecializeinfer_meta - s = LazyString("`promote_type(", i1, ", ", i2, ")` failed, there are conflicting `promote_rule` definitions for types ", left, ", ", right) - throw(ArgumentError(s)) + throw(_promote_type_binary_recursion_depth_limit_exception) end - function throw_gave_up((@nospecialize i1::Type), (@nospecialize i2::Type), (@nospecialize left::Type), (@nospecialize right::Type)) - @noinline - @_nospecializeinfer_meta - s = LazyString("`promote_type(", i1, ", ", i2, ")` failed, ended up with (", left, ", ", right, "), check for faulty/conflicting/missing `promote_rule` methods") - throw(ArgumentError(s)) + type_is_bottom(::Type{X}) where {X} = X === Bottom + function types_are_equal(::Type{A}, ::Type{B}) where {A,B} + @_total_meta + ccall(:jl_types_equal, Cint, (Any, Any), A, B) !== Cint(0) end - left = T - right = S - for _ ∈ 1:50 # must be finite to guarantee local termination - if is_bottom(left) - return right - end - if is_bottom(right) || types_are_equal(left, right) - return left - end - # Try `promote_rule` in both orders. - a = normalized_promote_rule(left, right) - b = normalized_promote_rule(right, left) - if ( - (types_are_equal(left, a) && types_are_equal(right, b)) || - (types_are_equal(left, b) && types_are_equal(right, a)) - ) - throw_conflicting_promote_rules(T, S, left, right) - end - if is_bottom(a) && is_bottom(b) - # If no `promote_rule` is defined, both directions give `Bottom`. In that - # case use `typejoin` on the original types. - return normalized_type(typejoin(left, right)) - end - left = a - right = b + normalize_type(::Type{X}) where {X} = X + if type_is_bottom(T) + return S + end + if type_is_bottom(S) || types_are_equal(S, T) + return T + end + if recursion_depth_limit === () + err() end - throw_gave_up(T, S, left, right) + l = tail(recursion_depth_limit) + # Try promote_rule in both orders. + st = normalize_type(promote_rule(S, T)) + ts = normalize_type(promote_rule(T, S)) + # If no promote_rule is defined, both directions give Bottom. In that + # case use typejoin on the original types instead. + if type_is_bottom(st) && type_is_bottom(ts) + normalize_type(typejoin(T, S)) + else + _promote_type_binary(ts, st, l) + end +end + +""" + _promote_type_binary_recursion_depth_limit::Tuple{Vararg{Nothing}} + +Recursion depth limit for `_promote_type_binary`, to prevent stack overflow. +""" +const _promote_type_binary_recursion_depth_limit = let + n2 = (nothing, nothing) + n4 = (n2..., n2...) + n8 = (n4..., n4...) + (n8..., n2...) +end + +function promote_type(::Type{T}, ::Type{S}) where {T,S} + _promote_type_binary(T, S, _promote_type_binary_recursion_depth_limit) end """ diff --git a/test/core.jl b/test/core.jl index bf159100468b5..95af16a1d0bd3 100644 --- a/test/core.jl +++ b/test/core.jl @@ -2019,35 +2019,14 @@ g4731() = f4731() @test f4731() == "" @test g4731() == "" -@testset "type promotion" begin - @testset "conflicting promote_rule error, PR #57507" begin - struct PR57507A end - struct PR57507B end - struct PR57507C end - @testset "error with conflicting promote_rules" begin - Base.promote_rule(::Type{PR57507A}, ::Type{PR57507B}) = PR57507A - Base.promote_rule(::Type{PR57507B}, ::Type{PR57507A}) = PR57507B - @test_throws ArgumentError promote_type(PR57507A, PR57507B) - @test_throws ArgumentError promote_type(PR57507B, PR57507A) - end - @testset "unambiguous cases" begin - @test PR57507A === @inferred promote_type(PR57507A, PR57507A) - @test PR57507B === @inferred promote_type(PR57507B, PR57507B) - Base.promote_rule(::Type{PR57507C}, ::Type{PR57507A}) = PR57507C - Base.promote_rule(::Type{PR57507B}, ::Type{PR57507C}) = PR57507C - @test PR57507C === @inferred promote_type(PR57507A, PR57507C) - @test PR57507C === @inferred promote_type(PR57507C, PR57507B) - end - end - @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 ArgumentError promote_type(Issue13193_Interval{Int}, Issue13193_SIQuantity{Int}) - end +@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 ArgumentError promote_type(Issue13193_Interval{Int}, Issue13193_SIQuantity{Int}) end # issue #4675 From 55ee0cdb6280e17982b8bb6ad45d6cc3d97725ec Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Wed, 4 Jun 2025 12:00:37 +0200 Subject: [PATCH 25/51] decrease the recursion depth limit to see what will break If this is fine with the test suite and PkgEval, we can raise it back for even greater assurance that there will be no breakage in practice. --- base/promotion.jl | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index ff083dc174923..ecfcc7e863f3e 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -351,9 +351,7 @@ Recursion depth limit for `_promote_type_binary`, to prevent stack overflow. """ const _promote_type_binary_recursion_depth_limit = let n2 = (nothing, nothing) - n4 = (n2..., n2...) - n8 = (n4..., n4...) - (n8..., n2...) + (n2..., n2...) end function promote_type(::Type{T}, ::Type{S}) where {T,S} From aae3d48fe4f8121991a4d3b058f551ec6a1ce36c Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Wed, 4 Jun 2025 12:03:49 +0200 Subject: [PATCH 26/51] stylistic change in preparation for next commit --- base/promotion.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index ecfcc7e863f3e..4c74241e85164 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -338,10 +338,9 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple # If no promote_rule is defined, both directions give Bottom. In that # case use typejoin on the original types instead. if type_is_bottom(st) && type_is_bottom(ts) - normalize_type(typejoin(T, S)) - else - _promote_type_binary(ts, st, l) + return normalize_type(typejoin(T, S)) end + _promote_type_binary(ts, st, l) end """ From 0cb2c5635d1c5d77e4e37a4b847423eefcf39d96 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Wed, 4 Jun 2025 12:06:01 +0200 Subject: [PATCH 27/51] move the recursion depth check to an even later point --- base/promotion.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 4c74241e85164..b099537f87eb4 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -328,10 +328,6 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple if type_is_bottom(S) || types_are_equal(S, T) return T end - if recursion_depth_limit === () - err() - end - l = tail(recursion_depth_limit) # Try promote_rule in both orders. st = normalize_type(promote_rule(S, T)) ts = normalize_type(promote_rule(T, S)) @@ -340,6 +336,10 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple if type_is_bottom(st) && type_is_bottom(ts) return normalize_type(typejoin(T, S)) end + if recursion_depth_limit === () + err() + end + l = tail(recursion_depth_limit) _promote_type_binary(ts, st, l) end From 294caffb63d7cd25bbbf3b034f9dc3516ae30d18 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Wed, 4 Jun 2025 12:07:04 +0200 Subject: [PATCH 28/51] rename `err` to `err_giving_up` --- base/promotion.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index b099537f87eb4..7ed74290b46b9 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -312,7 +312,7 @@ const _promote_type_binary_recursion_depth_limit_exception = let ArgumentError(s) end function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple{Vararg{Nothing}}) where {T,S} - function err() + function err_giving_up() @noinline throw(_promote_type_binary_recursion_depth_limit_exception) end @@ -337,7 +337,7 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple return normalize_type(typejoin(T, S)) end if recursion_depth_limit === () - err() + err_giving_up() end l = tail(recursion_depth_limit) _promote_type_binary(ts, st, l) From 782340c5e77b38ed4878f91cc47a8849c122b5c6 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Wed, 4 Jun 2025 12:17:18 +0200 Subject: [PATCH 29/51] detect the simplest kind of infinite recursion, improving UX --- base/promotion.jl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/base/promotion.jl b/base/promotion.jl index 7ed74290b46b9..ceeacd4f53d73 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -311,17 +311,26 @@ const _promote_type_binary_recursion_depth_limit_exception = let s = "`promote_type`: recursion depth limit reached, giving up; check for faulty/conflicting/missing `promote_rule` methods" ArgumentError(s) end +const _promote_type_binary_detected_infinite_recursion_exception = let + s = "`promote_type`: detected unbounded recursion caused by faulty `promote_rule` logic" + ArgumentError(s) +end function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple{Vararg{Nothing}}) where {T,S} function err_giving_up() @noinline throw(_promote_type_binary_recursion_depth_limit_exception) end + function err_detected_infinite_recursion() + @noinline + throw(_promote_type_binary_detected_infinite_recursion_exception) + end type_is_bottom(::Type{X}) where {X} = X === Bottom function types_are_equal(::Type{A}, ::Type{B}) where {A,B} @_total_meta ccall(:jl_types_equal, Cint, (Any, Any), A, B) !== Cint(0) end normalize_type(::Type{X}) where {X} = X + detect_loop(::Type{A}, ::Type{B}) where {A, B} = types_are_equal(T, A) && types_are_equal(S, B) if type_is_bottom(T) return S end @@ -336,6 +345,11 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple if type_is_bottom(st) && type_is_bottom(ts) return normalize_type(typejoin(T, S)) end + if detect_loop(ts, st) || detect_loop(st, ts) + # This is not strictly necessary, as we already limit the recursion depth, but + # makes for nicer UX. + err_detected_infinite_recursion() + end if recursion_depth_limit === () err_giving_up() end From de617d0f3a663eba78ffc0bf1b238e9a34e921db Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Wed, 4 Jun 2025 12:29:21 +0200 Subject: [PATCH 30/51] deduplicate --- base/operators.jl | 2 +- base/promotion.jl | 15 +++++++++------ 2 files changed, 10 insertions(+), 7 deletions(-) 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 ceeacd4f53d73..0b5a7c4f3505b 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -258,6 +258,13 @@ 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 + ## promotion mechanism ## """ @@ -325,16 +332,12 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple throw(_promote_type_binary_detected_infinite_recursion_exception) end type_is_bottom(::Type{X}) where {X} = X === Bottom - function types_are_equal(::Type{A}, ::Type{B}) where {A,B} - @_total_meta - ccall(:jl_types_equal, Cint, (Any, Any), A, B) !== Cint(0) - end normalize_type(::Type{X}) where {X} = X - detect_loop(::Type{A}, ::Type{B}) where {A, B} = types_are_equal(T, A) && types_are_equal(S, B) + detect_loop(::Type{A}, ::Type{B}) where {A, B} = _types_are_equal(T, A) && _types_are_equal(S, B) if type_is_bottom(T) return S end - if type_is_bottom(S) || types_are_equal(S, T) + if type_is_bottom(S) || _types_are_equal(S, T) return T end # Try promote_rule in both orders. From 4ad64967b251bd204e714d151724318f82d03ccd Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Wed, 4 Jun 2025 14:11:58 +0200 Subject: [PATCH 31/51] add back simple conflict test, should improve code coverage --- test/core.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/core.jl b/test/core.jl index 95af16a1d0bd3..900e2338d6a38 100644 --- a/test/core.jl +++ b/test/core.jl @@ -2028,6 +2028,14 @@ g4731() = f4731() Base.promote_rule(::Type{Issue13193_Interval{T}}, ::Type{S}) where {T, S<:Number} = Issue13193_Interval{promote_type(T,S)} @test_throws ArgumentError promote_type(Issue13193_Interval{Int}, Issue13193_SIQuantity{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 ArgumentError promote_type(ConflictingPromoteRuleDefinitionsA, ConflictingPromoteRuleDefinitionsB) + @test_throws ArgumentError promote_type(ConflictingPromoteRuleDefinitionsB, ConflictingPromoteRuleDefinitionsA) +end # issue #4675 f4675(x::StridedArray...) = 1 From 4e1700a92b17546ed1a1438ec018f3b4d62a96da Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Thu, 5 Jun 2025 11:42:12 +0200 Subject: [PATCH 32/51] deduplicate `normalize_type(promote_rule(A, B))` --- base/promotion.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 0b5a7c4f3505b..d3a9461c1e59c 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -333,6 +333,7 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple end type_is_bottom(::Type{X}) where {X} = X === Bottom normalize_type(::Type{X}) where {X} = X + normalize_promote_rule(::Type{A}, ::Type{B}) where {A, B} = normalize_type(promote_rule(A, B)) detect_loop(::Type{A}, ::Type{B}) where {A, B} = _types_are_equal(T, A) && _types_are_equal(S, B) if type_is_bottom(T) return S @@ -341,8 +342,8 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple return T end # Try promote_rule in both orders. - st = normalize_type(promote_rule(S, T)) - ts = normalize_type(promote_rule(T, S)) + st = normalize_promote_rule(S, T) + ts = normalize_promote_rule(T, S) # If no promote_rule is defined, both directions give Bottom. In that # case use typejoin on the original types instead. if type_is_bottom(st) && type_is_bottom(ts) From 195e8086c72d3608d4b44bd21b0252f65dba4e1a Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Thu, 5 Jun 2025 11:48:23 +0200 Subject: [PATCH 33/51] run `promote_rule(T, S)` before `promote_rule(S, T)` Ideally this shouldn't matter, however it's an easy fix for the test suite of MaxPlus.jl, which expects this ordering of the calls in an overly-strict `test_throws` test. --- base/promotion.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index d3a9461c1e59c..0ca7f7683e305 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -342,8 +342,8 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple return T end # Try promote_rule in both orders. - st = normalize_promote_rule(S, T) ts = normalize_promote_rule(T, S) + st = normalize_promote_rule(S, T) # If no promote_rule is defined, both directions give Bottom. In that # case use typejoin on the original types instead. if type_is_bottom(st) && type_is_bottom(ts) From 93af1fb5eaac377bad754a5bffb5ace8b7b6ebcf Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Thu, 5 Jun 2025 12:09:39 +0200 Subject: [PATCH 34/51] add back the four `promote_type` methods for now to make PkgEval clean Will remove them in a follow-up PR. Just adding them back to make this PR easier to review. --- base/promotion.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/base/promotion.jl b/base/promotion.jl index 0ca7f7683e305..a9de4f84f094f 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -314,6 +314,15 @@ 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...)) +# The following four methods are kept temporarily to prevent breaking a broken registered +# package, PolynomialRings.jl. Deleting these methods causes dispatch ambiguity errors +# for PolynomialRings.jl, because PolynomialRings.jl adds method to `promote_type`, which +# is not allowed. +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 + const _promote_type_binary_recursion_depth_limit_exception = let s = "`promote_type`: recursion depth limit reached, giving up; check for faulty/conflicting/missing `promote_rule` methods" ArgumentError(s) From 103d1aeeba10fc4baf8e0ac141269a4cbd710a0c Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Thu, 5 Jun 2025 12:18:37 +0200 Subject: [PATCH 35/51] add NEWS entry --- NEWS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/NEWS.md b/NEWS.md index 1e4e24b67a4db..d703a37a52437 100644 --- a/NEWS.md +++ b/NEWS.md @@ -16,6 +16,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. ([#57517]) + Compiler/Runtime improvements ----------------------------- From 799824bad97a470849095f1a1a6d0d1d674bc015 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 6 Jun 2025 14:10:54 +0200 Subject: [PATCH 36/51] increase the depth limit to eight --- base/promotion.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index a9de4f84f094f..45b1e339dfc25 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -377,7 +377,9 @@ Recursion depth limit for `_promote_type_binary`, to prevent stack overflow. """ const _promote_type_binary_recursion_depth_limit = let n2 = (nothing, nothing) - (n2..., n2...) + n4 = (n2..., n2...) + n8 = (n4..., n4...) + n8 end function promote_type(::Type{T}, ::Type{S}) where {T,S} From b7164cd83955952768d06d9ea5d79114c8663804 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 07:55:10 +0200 Subject: [PATCH 37/51] do away with `normalize_type` --- base/promotion.jl | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 45b1e339dfc25..5153940682ef0 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -341,8 +341,6 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple throw(_promote_type_binary_detected_infinite_recursion_exception) end type_is_bottom(::Type{X}) where {X} = X === Bottom - normalize_type(::Type{X}) where {X} = X - normalize_promote_rule(::Type{A}, ::Type{B}) where {A, B} = normalize_type(promote_rule(A, B)) detect_loop(::Type{A}, ::Type{B}) where {A, B} = _types_are_equal(T, A) && _types_are_equal(S, B) if type_is_bottom(T) return S @@ -351,12 +349,12 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple return T end # Try promote_rule in both orders. - ts = normalize_promote_rule(T, S) - st = normalize_promote_rule(S, T) + 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. if type_is_bottom(st) && type_is_bottom(ts) - return normalize_type(typejoin(T, S)) + return typejoin(T, S) end if detect_loop(ts, st) || detect_loop(st, ts) # This is not strictly necessary, as we already limit the recursion depth, but From 80e03a1d9b2e27a79a3e581bc91848d6916f7e91 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 08:00:02 +0200 Subject: [PATCH 38/51] one less method static parameter --- base/promotion.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index 5153940682ef0..472aaf9f17359 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -340,7 +340,7 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple @noinline throw(_promote_type_binary_detected_infinite_recursion_exception) end - type_is_bottom(::Type{X}) where {X} = X === Bottom + type_is_bottom(X::Type) = X <: Bottom detect_loop(::Type{A}, ::Type{B}) where {A, B} = _types_are_equal(T, A) && _types_are_equal(S, B) if type_is_bottom(T) return S From 124acec677608679f31b727cd78baac02904b92b Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 08:05:06 +0200 Subject: [PATCH 39/51] even less method static parameters --- base/promotion.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 472aaf9f17359..79ad4e5f2614e 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -331,7 +331,7 @@ const _promote_type_binary_detected_infinite_recursion_exception = let s = "`promote_type`: detected unbounded recursion caused by faulty `promote_rule` logic" ArgumentError(s) end -function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple{Vararg{Nothing}}) where {T,S} +function _promote_type_binary(T::Type, S::Type, recursion_depth_limit::Tuple{Vararg{Nothing}}) function err_giving_up() @noinline throw(_promote_type_binary_recursion_depth_limit_exception) @@ -341,7 +341,7 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple throw(_promote_type_binary_detected_infinite_recursion_exception) end type_is_bottom(X::Type) = X <: Bottom - detect_loop(::Type{A}, ::Type{B}) where {A, B} = _types_are_equal(T, A) && _types_are_equal(S, B) + detect_loop(T::Type, S::Type, A::Type, B::Type) = _types_are_equal(T, A) && _types_are_equal(S, B) if type_is_bottom(T) return S end @@ -356,7 +356,7 @@ function _promote_type_binary(::Type{T}, ::Type{S}, recursion_depth_limit::Tuple if type_is_bottom(st) && type_is_bottom(ts) return typejoin(T, S) end - if detect_loop(ts, st) || detect_loop(st, ts) + if detect_loop(T, S, ts, st) || detect_loop(T, S, st, ts) # This is not strictly necessary, as we already limit the recursion depth, but # makes for nicer UX. err_detected_infinite_recursion() From 229efbbd80d121dc6323ffae1397bf3cdccb0fd8 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 08:44:01 +0200 Subject: [PATCH 40/51] slight refactor --- base/promotion.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 79ad4e5f2614e..a1f8bdc2613df 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -341,7 +341,8 @@ function _promote_type_binary(T::Type, S::Type, recursion_depth_limit::Tuple{Var throw(_promote_type_binary_detected_infinite_recursion_exception) end type_is_bottom(X::Type) = X <: Bottom - detect_loop(T::Type, S::Type, A::Type, B::Type) = _types_are_equal(T, A) && _types_are_equal(S, B) + detect_loop_onesided(T::Type, S::Type, A::Type, B::Type) = _types_are_equal(T, A) && _types_are_equal(S, B) + detect_loop(T::Type, S::Type, A::Type, B::Type) = detect_loop_onesided(T, S, A, B) || detect_loop_onesided(T, S, B, A) if type_is_bottom(T) return S end @@ -356,7 +357,7 @@ function _promote_type_binary(T::Type, S::Type, recursion_depth_limit::Tuple{Var if type_is_bottom(st) && type_is_bottom(ts) return typejoin(T, S) end - if detect_loop(T, S, ts, st) || detect_loop(T, S, st, ts) + if detect_loop(T, S, ts, st) # This is not strictly necessary, as we already limit the recursion depth, but # makes for nicer UX. err_detected_infinite_recursion() From e762e6e146d208428d73196ddec2527051a69f6e Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 08:50:02 +0200 Subject: [PATCH 41/51] `type_is_bottom` -> `_type_is_bottom` --- base/promotion.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index a1f8bdc2613df..de974a07d0045 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -265,6 +265,10 @@ function _types_are_equal(A::Type, B::Type) ccall(:jl_types_equal, Cint, (Any, Any), A, B) !== Cint(0) end +function _type_is_bottom(X::Type) + X <: Bottom +end + ## promotion mechanism ## """ @@ -340,13 +344,12 @@ function _promote_type_binary(T::Type, S::Type, recursion_depth_limit::Tuple{Var @noinline throw(_promote_type_binary_detected_infinite_recursion_exception) end - type_is_bottom(X::Type) = X <: Bottom detect_loop_onesided(T::Type, S::Type, A::Type, B::Type) = _types_are_equal(T, A) && _types_are_equal(S, B) detect_loop(T::Type, S::Type, A::Type, B::Type) = detect_loop_onesided(T, S, A, B) || detect_loop_onesided(T, S, B, A) - if type_is_bottom(T) + if _type_is_bottom(T) return S end - if type_is_bottom(S) || _types_are_equal(S, T) + if _type_is_bottom(S) || _types_are_equal(S, T) return T end # Try promote_rule in both orders. @@ -354,7 +357,7 @@ function _promote_type_binary(T::Type, S::Type, recursion_depth_limit::Tuple{Var 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. - if type_is_bottom(st) && type_is_bottom(ts) + if _type_is_bottom(st) && _type_is_bottom(ts) return typejoin(T, S) end if detect_loop(T, S, ts, st) From 9e78161a3b8e5892faa4ae21741c71b6284c8f28 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 08:58:52 +0200 Subject: [PATCH 42/51] reorder operations so some of them come before the recursive part --- base/promotion.jl | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index de974a07d0045..6514f34f0c082 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -346,20 +346,22 @@ function _promote_type_binary(T::Type, S::Type, recursion_depth_limit::Tuple{Var end detect_loop_onesided(T::Type, S::Type, A::Type, B::Type) = _types_are_equal(T, A) && _types_are_equal(S, B) detect_loop(T::Type, S::Type, A::Type, B::Type) = detect_loop_onesided(T, S, A, B) || detect_loop_onesided(T, S, B, A) - if _type_is_bottom(T) - return S - end - if _type_is_bottom(S) || _types_are_equal(S, T) - return T - end # 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. - if _type_is_bottom(st) && _type_is_bottom(ts) + 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 detect_loop(T, S, ts, st) # This is not strictly necessary, as we already limit the recursion depth, but # makes for nicer UX. @@ -385,6 +387,12 @@ const _promote_type_binary_recursion_depth_limit = let end function promote_type(::Type{T}, ::Type{S}) where {T,S} + 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, _promote_type_binary_recursion_depth_limit) end From ea704c1168c504a554f81882c83af4ab72f00aab Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 09:28:08 +0200 Subject: [PATCH 43/51] local functions to global functions In preparation for replacing recursion with metaprogramming. --- base/promotion.jl | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 6514f34f0c082..b140a84b00bea 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -335,17 +335,19 @@ const _promote_type_binary_detected_infinite_recursion_exception = let s = "`promote_type`: detected unbounded recursion caused by faulty `promote_rule` logic" ArgumentError(s) end +function _promote_type_binary_err_giving_up() + @noinline + throw(_promote_type_binary_recursion_depth_limit_exception) +end +function _promote_type_binary_err_detected_infinite_recursion() + @noinline + throw(_promote_type_binary_detected_infinite_recursion_exception) +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 function _promote_type_binary(T::Type, S::Type, recursion_depth_limit::Tuple{Vararg{Nothing}}) - function err_giving_up() - @noinline - throw(_promote_type_binary_recursion_depth_limit_exception) - end - function err_detected_infinite_recursion() - @noinline - throw(_promote_type_binary_detected_infinite_recursion_exception) - end - detect_loop_onesided(T::Type, S::Type, A::Type, B::Type) = _types_are_equal(T, A) && _types_are_equal(S, B) - detect_loop(T::Type, S::Type, A::Type, B::Type) = detect_loop_onesided(T, S, A, B) || detect_loop_onesided(T, S, B, A) # Try promote_rule in both orders. ts = promote_rule(T, S) st = promote_rule(S, T) @@ -362,13 +364,13 @@ function _promote_type_binary(T::Type, S::Type, recursion_depth_limit::Tuple{Var if st_is_bottom || _types_are_equal(st, ts) return ts end - if detect_loop(T, S, ts, st) + 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. - err_detected_infinite_recursion() + _promote_type_binary_err_detected_infinite_recursion() end if recursion_depth_limit === () - err_giving_up() + _promote_type_binary_err_giving_up() end l = tail(recursion_depth_limit) _promote_type_binary(ts, st, l) From d77ce17ab88d0872935b32c393e7d2b8fea3260e Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 09:42:12 +0200 Subject: [PATCH 44/51] replace recursion with metaprogramming, enabling stronger inference In particular, this enables more calls to be constant folded. --- base/promotion.jl | 76 +++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index b140a84b00bea..90e72d5810223 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -347,45 +347,45 @@ 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 -function _promote_type_binary(T::Type, S::Type, recursion_depth_limit::Tuple{Vararg{Nothing}}) - # 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() - end - if recursion_depth_limit === () - _promote_type_binary_err_giving_up() +macro _promote_type_binary_step() + 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() + end + T = ts + S = st end - l = tail(recursion_depth_limit) - _promote_type_binary(ts, st, l) + esc(e) end - -""" - _promote_type_binary_recursion_depth_limit::Tuple{Vararg{Nothing}} - -Recursion depth limit for `_promote_type_binary`, to prevent stack overflow. -""" -const _promote_type_binary_recursion_depth_limit = let - n2 = (nothing, nothing) - n4 = (n2..., n2...) - n8 = (n4..., n4...) - n8 +function _promote_type_binary(T::Type, S::Type) + @_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_step + + _promote_type_binary_err_giving_up() end function promote_type(::Type{T}, ::Type{S}) where {T,S} @@ -395,7 +395,7 @@ function promote_type(::Type{T}, ::Type{S}) where {T,S} if _type_is_bottom(S) || _types_are_equal(S, T) return T end - _promote_type_binary(T, S, _promote_type_binary_recursion_depth_limit) + _promote_type_binary(T, S) end """ From 715fd7cbb3e9931dbb29c60e93f16e408b8dba44 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 10:06:30 +0200 Subject: [PATCH 45/51] expand tests --- test/core.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/core.jl b/test/core.jl index 900e2338d6a38..9ef62ee7acaa5 100644 --- a/test/core.jl +++ b/test/core.jl @@ -2027,6 +2027,9 @@ g4731() = f4731() 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 ArgumentError promote_type(Issue13193_Interval{Int}, Issue13193_SIQuantity{Int}) + @test_throws ArgumentError 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 @@ -2035,6 +2038,8 @@ end Base.promote_rule(::Type{ConflictingPromoteRuleDefinitionsB}, ::Type{ConflictingPromoteRuleDefinitionsA}) = ConflictingPromoteRuleDefinitionsB @test_throws ArgumentError promote_type(ConflictingPromoteRuleDefinitionsA, ConflictingPromoteRuleDefinitionsB) @test_throws ArgumentError 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 From 14bc5a16573519da82bea9e1aae68c7c6d6c9c38 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 10:12:04 +0200 Subject: [PATCH 46/51] update NEWS --- NEWS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.md b/NEWS.md index d703a37a52437..84f5ef3226a3e 100644 --- a/NEWS.md +++ b/NEWS.md @@ -16,7 +16,7 @@ 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. ([#57517]) + - 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]) Compiler/Runtime improvements ----------------------------- From 1cb1cb6ba37fe6a0f1d04f0a432d87589ab9e156 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 10:57:55 +0200 Subject: [PATCH 47/51] introduce custom exception types to improve error messages --- base/promotion.jl | 83 ++++++++++++++++++++++++++++++++++++++++------- test/core.jl | 8 ++--- 2 files changed, 75 insertions(+), 16 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 90e72d5810223..4b405ba6d94eb 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -327,26 +327,81 @@ 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 -const _promote_type_binary_recursion_depth_limit_exception = let - s = "`promote_type`: recursion depth limit reached, giving up; check for faulty/conflicting/missing `promote_rule` methods" - ArgumentError(s) +""" + 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 -const _promote_type_binary_detected_infinite_recursion_exception = let - s = "`promote_type`: detected unbounded recursion caused by faulty `promote_rule` logic" - ArgumentError(s) + +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() + +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 - throw(_promote_type_binary_recursion_depth_limit_exception) + @_nospecializeinfer_meta + throw(TypePromotionError(T_initial, S_initial, T, S, ts, st, true)) end -function _promote_type_binary_err_detected_infinite_recursion() +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 - throw(_promote_type_binary_detected_infinite_recursion_exception) + @_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_step() e = quote # Try promote_rule in both orders. @@ -368,14 +423,18 @@ macro _promote_type_binary_step() 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() + _promote_type_binary_err_detected_infinite_recursion(T_initial, S_initial, T, S, ts, st) end T = ts S = st end esc(e) end + function _promote_type_binary(T::Type, S::Type) + T_initial = T + S_initial = S + @_promote_type_binary_step @_promote_type_binary_step @_promote_type_binary_step @@ -385,7 +444,7 @@ function _promote_type_binary(T::Type, S::Type) @_promote_type_binary_step @_promote_type_binary_step - _promote_type_binary_err_giving_up() + _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} diff --git a/test/core.jl b/test/core.jl index 9ef62ee7acaa5..8cbf6a8e1ba51 100644 --- a/test/core.jl +++ b/test/core.jl @@ -2026,8 +2026,8 @@ g4731() = f4731() 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 ArgumentError promote_type(Issue13193_Interval{Int}, Issue13193_SIQuantity{Int}) - @test_throws ArgumentError promote_type(Issue13193_SIQuantity{Int}, Issue13193_Interval{Int}) + @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 @@ -2036,8 +2036,8 @@ end struct ConflictingPromoteRuleDefinitionsB end Base.promote_rule(::Type{ConflictingPromoteRuleDefinitionsA}, ::Type{ConflictingPromoteRuleDefinitionsB}) = ConflictingPromoteRuleDefinitionsA Base.promote_rule(::Type{ConflictingPromoteRuleDefinitionsB}, ::Type{ConflictingPromoteRuleDefinitionsA}) = ConflictingPromoteRuleDefinitionsB - @test_throws ArgumentError promote_type(ConflictingPromoteRuleDefinitionsA, ConflictingPromoteRuleDefinitionsB) - @test_throws ArgumentError promote_type(ConflictingPromoteRuleDefinitionsB, ConflictingPromoteRuleDefinitionsA) + @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 From 55d5921e121d581f93fd6665d93ed70f650a5fdc Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 21:55:57 +0200 Subject: [PATCH 48/51] fix thrown exception when reaching depth limit --- base/promotion.jl | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 4b405ba6d94eb..04298d2602874 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -402,7 +402,7 @@ function _promote_type_binary_detect_loop(T::Type, S::Type, A::Type, B::Type) onesided(T, S, A, B) || onesided(T, S, B, A) end -macro _promote_type_binary_step() +macro _promote_type_binary_step1() e = quote # Try promote_rule in both orders. ts = promote_rule(T, S) @@ -425,12 +425,26 @@ macro _promote_type_binary_step() # 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(T::Type, S::Type) T_initial = T S_initial = S @@ -442,7 +456,8 @@ function _promote_type_binary(T::Type, S::Type) @_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 From 011d7a1b79b4f4edb37f35bcefacf3172749355a Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 22:08:45 +0200 Subject: [PATCH 49/51] fix effect inference issues: more specialization --- base/promotion.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 04298d2602874..28c617aaa6eff 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -445,9 +445,9 @@ macro _promote_type_binary_step() esc(e) end -function _promote_type_binary(T::Type, S::Type) - T_initial = T - S_initial = S +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 From 1d8d891f288d2e562b89211b1c6dde6e0d65bb8a Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 22:35:13 +0200 Subject: [PATCH 50/51] simplify "type is bottom" check --- base/promotion.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/promotion.jl b/base/promotion.jl index 28c617aaa6eff..819be30211ed7 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -266,7 +266,7 @@ function _types_are_equal(A::Type, B::Type) end function _type_is_bottom(X::Type) - X <: Bottom + X === Bottom end ## promotion mechanism ## From 7926227275516b22db4e24a00c661c5e8fff0658 Mon Sep 17 00:00:00 2001 From: Neven Sajko Date: Fri, 13 Jun 2025 23:55:01 +0200 Subject: [PATCH 51/51] delete the unused `promote_type` methods again The registered package in question will be broken for an unrelated reason now anyway by PR #58720, so there's no sense in trying to protect it any more. In any case, it doesn't make sense to hold back the language just to protect a single broken package. --- base/promotion.jl | 9 --------- 1 file changed, 9 deletions(-) diff --git a/base/promotion.jl b/base/promotion.jl index 819be30211ed7..cf6fe2b8e53c5 100644 --- a/base/promotion.jl +++ b/base/promotion.jl @@ -318,15 +318,6 @@ 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...)) -# The following four methods are kept temporarily to prevent breaking a broken registered -# package, PolynomialRings.jl. Deleting these methods causes dispatch ambiguity errors -# for PolynomialRings.jl, because PolynomialRings.jl adds method to `promote_type`, which -# is not allowed. -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