Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

limit recursion depth to prevent stack overflow in 2-arg promote_type #57517

Open
wants to merge 18 commits into
base: master
Choose a base branch
from

Conversation

nsajko
Copy link
Contributor

@nsajko nsajko commented Feb 24, 2025

Limit recursion depth to prevent stack overflows.

Fixes #13193

@nsajko nsajko added the bugfix This change fixes an existing bug label Feb 24, 2025
@nsajko nsajko force-pushed the prevent_stack_overflow_in_type_promotion branch from 57dbdeb to 69e5d32 Compare February 24, 2025 14:31
@adienes
Copy link
Contributor

adienes commented Feb 24, 2025

what is the advantage over #57507 ? this seems to add quite a bit more complexity and includes scary things like hardcoded constants

@nsajko
Copy link
Contributor Author

nsajko commented Feb 24, 2025

what is the advantage over #57507 ?

This PR prevents a much wider class of stack overflows. In particular, #57507 doesn't fix #13193 (see the test here for an example), while this PR should.

scary things like hardcoded constants

Usually I avoid hardcoded constants as best as I can, but in this case there's no way to guarantee (local) termination without hardcoding a cutoff. Another approach would be to stick with recursion, but limit the depth, however in any case it definitely seems necessary to have some kind of hardcoded limit.

promote_type shouldn't run without bound just because of faulty/conflicting promote_rules. Or, worse, overflow the stack. Or, even worse, cause the compiler itself to run without bound while it's trying to compile the promote_type method.

@nsajko nsajko force-pushed the prevent_stack_overflow_in_type_promotion branch from 9e1f5dc to 4b7bb02 Compare February 24, 2025 22:00
@nsajko nsajko added the backport 1.12 Change should be backported to release-1.12 label Feb 25, 2025
@nsajko
Copy link
Contributor Author

nsajko commented Feb 25, 2025

This is how the behavior for #13193 looks with this PR, due to the limited recursion depth:

julia> struct SIQuantity{T<:Number} <: Number; end

julia> Base.promote_rule(::Type{SIQuantity{T}}, ::Type{SIQuantity{S}}) where {T, S} = SIQuantity{promote_type(T,S)}

julia> Base.promote_rule(::Type{SIQuantity{T}}, ::Type{S}) where {T, S<:Number} = SIQuantity{promote_type(T,S)}

julia> struct Interval{T<:Number} <: Number; end

julia> Base.promote_rule(::Type{Interval{T}}, ::Type{Interval{S}}) where {T, S} = Interval{promote_type(T,S)}

julia> Base.promote_rule(::Type{Interval{T}}, ::Type{S}) where {T, S<:Number} = Interval{promote_type(T,S)}

julia> promote_type(Interval{Int}, SIQuantity{Int})
ERROR: ArgumentError: `promote_type`: recursion depth limit reached, giving up; check for faulty/conflicting/missing `promote_rule` methods
Stacktrace:
  [1] _promote_type_binary(::Type, ::Type, ::Tuple{})
    @ Base ./promotion.jl:308
  [2] promote_result(::Type, ::Type, ::Type{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{Int64}}}}}}}}}}}}, ::Type{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Int64}}}}}}}}}}}}, l::Tuple{})
    @ Base ./promotion.jl:360
  [3] _promote_type_binary(::Type{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Int64}}}}}}}}}}}, ::Type{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{Int64}}}}}}}}}}}, recursion_depth_limit::Tuple{Nothing})
    @ Base ./promotion.jl:325
  [4] promote_result(::Type, ::Type, ::Type{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Int64}}}}}}}}}}}, ::Type{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{Int64}}}}}}}}}}}, l::Tuple{Nothing})
    @ Base ./promotion.jl:360
  [5] _promote_type_binary(::Type{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{Int64}}}}}}}}}}, ::Type{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Int64}}}}}}}}}}, recursion_depth_limit::Tuple{Nothing, Nothing})
    @ Base ./promotion.jl:325
  [6] promote_result(::Type, ::Type, ::Type{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{Int64}}}}}}}}}}, ::Type{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Int64}}}}}}}}}}, l::Tuple{Nothing, Nothing})
    @ Base ./promotion.jl:360
  [7] _promote_type_binary
    @ ./promotion.jl:325 [inlined]
  [8] promote_result(::Type, ::Type, ::Type{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Int64}}}}}}}}}, ::Type{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{Int64}}}}}}}}}, l::Tuple{Nothing, Nothing, Nothing})
    @ Base ./promotion.jl:360
  [9] _promote_type_binary
    @ ./promotion.jl:325 [inlined]
 [10] promote_result(::Type, ::Type, ::Type{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{Int64}}}}}}}}, ::Type{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Int64}}}}}}}}, l::NTuple{4, Nothing})
    @ Base ./promotion.jl:360
 [11] _promote_type_binary
    @ ./promotion.jl:325 [inlined]
 [12] promote_result(::Type, ::Type, ::Type{Interval{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Int64}}}}}}}, ::Type{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Interval{Int64}}}}}}}, l::NTuple{5, Nothing})
    @ Base ./promotion.jl:360
 [13] _promote_type_binary
    @ ./promotion.jl:325 [inlined]
 [14] promote_result(::Type, ::Type, ::Type{Interval{SIQuantity{Interval{SIQuantity{Interval{Int64}}}}}}, ::Type{SIQuantity{Interval{SIQuantity{Interval{SIQuantity{Int64}}}}}}, l::NTuple{6, Nothing})
    @ Base ./promotion.jl:360
 [15] _promote_type_binary
    @ ./promotion.jl:325 [inlined]
 [16] promote_result(::Type, ::Type, ::Type{Interval{SIQuantity{Interval{SIQuantity{Int64}}}}}, ::Type{SIQuantity{Interval{SIQuantity{Interval{Int64}}}}}, l::NTuple{7, Nothing})
    @ Base ./promotion.jl:360
 [17] _promote_type_binary
    @ ./promotion.jl:325 [inlined]
 [18] promote_result(::Type, ::Type, ::Type{Interval{SIQuantity{Interval{Int64}}}}, ::Type{SIQuantity{Interval{SIQuantity{Int64}}}}, l::NTuple{8, Nothing})
    @ Base ./promotion.jl:360
 [19] _promote_type_binary
    @ ./promotion.jl:325 [inlined]
 [20] promote_result(::Type, ::Type, ::Type{Interval{SIQuantity{Int64}}}, ::Type{SIQuantity{Interval{Int64}}}, l::NTuple{9, Nothing})
    @ Base ./promotion.jl:360
 [21] _promote_type_binary
    @ ./promotion.jl:325 [inlined]
 [22] promote_type(::Type{Interval{Int64}}, ::Type{SIQuantity{Int64}})
    @ Base ./promotion.jl:340
 [23] top-level scope
    @ REPL[7]:1

@nsajko nsajko added error handling Handling of exceptions by Julia or the user error messages Better, more actionable error messages labels Feb 25, 2025
@nsajko nsajko requested review from aviatesk and adienes February 25, 2025 06:48
@adienes
Copy link
Contributor

adienes commented Feb 25, 2025

not sure if I'm 100% qualified to review beyond stylistic things but as far as I can tell I like this approach

it might make sense to run the benchmarks? I know this shouldn't change anything but promote_type is quite a highly used function so just in case

@nsajko nsajko added minor change Marginal behavior change acceptable for a minor release needs pkgeval Tests for all registered packages should be run with this change needs nanosoldier run This PR should have benchmarks run on it labels Feb 25, 2025
@nsajko nsajko changed the title prevent stack overflow in 2-arg promote_type limit recursion depth to prevent stack overflow in 2-arg promote_type Feb 25, 2025
@nsajko nsajko added triage This should be discussed on a triage call and removed backport 1.12 Change should be backported to release-1.12 labels Feb 25, 2025
@nsajko
Copy link
Contributor Author

nsajko commented Feb 25, 2025

Question for triage: does limiting the recursion depth seem like an acceptable minor change? It's a bugfix, as it prevents stack overflows, but hypothetically some user code could break if it relied on an awkward chain of promote_rules.

NB: an earlier version of this PR tried to do away with recursion altogether in favor of a loop, however that resulted in inference regressions.

NB: setting the recursion depth limit higher breaks bootstrap for some reason, so currently it's not possible to set the limit higher.

@nsajko nsajko force-pushed the prevent_stack_overflow_in_type_promotion branch from 6236d3f to fac1ce7 Compare February 26, 2025 20:24
@nsajko
Copy link
Contributor Author

nsajko commented Feb 26, 2025

@nanosoldier runtests()

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bugfix This change fixes an existing bug error handling Handling of exceptions by Julia or the user error messages Better, more actionable error messages minor change Marginal behavior change acceptable for a minor release needs nanosoldier run This PR should have benchmarks run on it needs pkgeval Tests for all registered packages should be run with this change triage This should be discussed on a triage call
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Infinite recursion when promote_rule depends on order of arguments
2 participants