From 15ab0262bf015ec9e41df89f718d6718c2bb1ee6 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Tue, 21 Nov 2023 02:59:58 +0900 Subject: [PATCH 1/5] inference: add reflection utility for exception type analysis This commit defines functions that mirror our tools for analyzing return types and computational effects. The key point to discuss is that this commit introduces two functions: `Base.exception_types` and `Base.exception_type`. `Base.exception_types` acts like `Base.return_types`, giving a list of exception types for each method that matches with the given call signature. On the other hand, `Base.exception_type` is akin to `Base.infer_effects`, returning a single exception type that covers all potential outcomes entailed by the given call signature. I personally lean towards the latter for its utility, particularly in testing scenarios, but I included `exception_types` too for consistency with `return_types`. I'd welcome any feedback on this approach. --- base/reflection.jl | 264 ++++++++++++++++++++++++++++++++++++++++----- test/reflection.jl | 27 +++++ 2 files changed, 263 insertions(+), 28 deletions(-) diff --git a/base/reflection.jl b/base/reflection.jl index 304f7a4344fb2..145a7a9a376d5 100644 --- a/base/reflection.jl +++ b/base/reflection.jl @@ -1692,15 +1692,45 @@ function code_ircode_by_type( return asts end +function _builtin_return_type(interp::Core.Compiler.AbstractInterpreter, + @nospecialize(f::Core.Builtin), @nospecialize(types)) + argtypes = Any[to_tuple_type(types).parameters...] + rt = Core.Compiler.builtin_tfunction(interp, f, argtypes, nothing) + return Core.Compiler.widenconst(rt) +end + +function _builtin_effects(interp::Core.Compiler.AbstractInterpreter, + @nospecialize(f::Core.Builtin), @nospecialize(types)) + argtypes = Any[to_tuple_type(types).parameters...] + rt = Core.Compiler.builtin_tfunction(interp, f, argtypes, nothing) + return Core.Compiler.builtin_effects(Core.Compiler.typeinf_lattice(interp), f, argtypes, rt) +end + +check_generated_context(world::UInt) = + (ccall(:jl_is_in_pure_context, Bool, ()) || world == typemax(UInt)) && + error("code reflection cannot be used from generated functions") """ - Base.return_types(f::Function, types::DataType=default_tt(f); - world::UInt=get_world_counter(), interp::NativeInterpreter=Core.Compiler.NativeInterpreter(world)) + Base.return_types( + f, types=default_tt(f); + world::UInt=get_world_counter(), + interp::NativeInterpreter=Core.Compiler.NativeInterpreter(world)) -> rts::Vector{Any} Return a list of possible return types for a given function `f` and argument types `types`. The list corresponds to the results of type inference on all the possible method match candidates for `f` and `types` (see also [`methods(f, types)`](@ref methods). +# Arguments +- `f`: The function to analyze. +- `types` (optional): The argument types of the function. Defaults to the default tuple type of `f`. +- `world` (optional): The world counter to use for the analysis. Defaults to the current world counter. +- `interp` (optional): The abstract interpreter to use for the analysis. Defaults to a new `Core.Compiler.NativeInterpreter` with the specified `world`. + +# Returns +- `rts::Vector{Any}`: The list of return types that are figured out by inference on + methods matching with the given `f` and `types`. The list's order matches the order + returned by `methods(f, types)`. + # Example ```julia @@ -1722,25 +1752,22 @@ julia> Base.return_types(sum, (Union{Vector{Int},UnitRange{Int}},)) ``` !!! warning - The `return_types` function should not be used from generated functions; + The `Base.return_types` function should not be used from generated functions; doing so will result in an error. """ function return_types(@nospecialize(f), @nospecialize(types=default_tt(f)); world::UInt=get_world_counter(), interp::Core.Compiler.AbstractInterpreter=Core.Compiler.NativeInterpreter(world)) - (ccall(:jl_is_in_pure_context, Bool, ()) || world == typemax(UInt)) && - error("code reflection cannot be used from generated functions") + check_generated_context(world) if isa(f, Core.OpaqueClosure) _, rt = only(code_typed_opaque_closure(f)) return Any[rt] end - if isa(f, Core.Builtin) - argtypes = Any[to_tuple_type(types).parameters...] - rt = Core.Compiler.builtin_tfunction(interp, f, argtypes, nothing) - return Any[Core.Compiler.widenconst(rt)] + rt = _builtin_return_type(interp, f, types) + return Any[rt] end - rts = [] + rts = Any[] tt = signature_type(f, types) matches = _methods_by_ftype(tt, #=lim=#-1, world)::Vector for match in matches @@ -1752,35 +1779,220 @@ function return_types(@nospecialize(f), @nospecialize(types=default_tt(f)); end """ - infer_effects(f, types=default_tt(f); world=get_world_counter(), interp=Core.Compiler.NativeInterpreter(world)) + Base.infer_exception_types( + f, types=default_tt(f); + world::UInt=get_world_counter(), + interp::NativeInterpreter=Core.Compiler.NativeInterpreter(world)) -> excts::Vector{Any} -Compute the `Effects` of a function `f` with argument types `types`. The `Effects` represents the computational effects of the function call, such as whether it is free of side effects, guaranteed not to throw an exception, guaranteed to terminate, etc. The `world` and `interp` arguments specify the world counter and the native interpreter to use for the analysis. +Return a list of possible exception types for a given function `f` and argument types `types`. +The list corresponds to the results of type inference on all the possible method match +candidates for `f` and `types` (see also [`methods(f, types)`](@ref methods). +It works like [`Base.return_types`](@ref), but it infers the exception types instead of the return types. # Arguments - `f`: The function to analyze. - `types` (optional): The argument types of the function. Defaults to the default tuple type of `f`. - `world` (optional): The world counter to use for the analysis. Defaults to the current world counter. -- `interp` (optional): The native interpreter to use for the analysis. Defaults to a new `Core.Compiler.NativeInterpreter` with the specified `world`. +- `interp` (optional): The abstract interpreter to use for the analysis. Defaults to a new `Core.Compiler.NativeInterpreter` with the specified `world`. # Returns -- `effects::Effects`: The computed effects of the function call. +- `excts::Vector{Any}`: The list of exception types that are figured out by inference on + methods matching with the given `f` and `types`. The list's order matches the order + returned by `methods(f, types)`. # Example ```julia -julia> function foo(x) - y = x * 2 - return y - end; +julia> throw_if_number(::Number) = error("number is given"); + +julia> throw_if_number(::Any) = nothing; + +julia> Base.infer_exception_types(throw_if_number, (Int,)) +1-element Vector{Any}: + ErrorException + +julia> methods(throw_if_number, (Any,)) +# 2 methods for generic function "throw_if_number" from Main: + [1] throw_if_number(x::Number) + @ REPL[1]:1 + [2] throw_if_number(::Any) + @ REPL[2]:1 + +julia> Base.infer_exception_types(throw_if_number, (Any,)) +2-element Vector{Any}: + ErrorException # the result of inference on `throw_if_number(::Number)` + Union{} # the result of inference on `throw_if_number(::Any)` +``` + +!!! warning + The `Base.infer_exception_types` function should not be used from generated functions; + doing so will result in an error. +""" +function infer_exception_types(@nospecialize(f), @nospecialize(types=default_tt(f)); + world::UInt=get_world_counter(), + interp::Core.Compiler.AbstractInterpreter=Core.Compiler.NativeInterpreter(world)) + check_generated_context(world) + if isa(f, Core.OpaqueClosure) + return Any[Any] # TODO + end + if isa(f, Core.Builtin) + effects = _builtin_effects(interp, f, types) + exct = Core.Compiler.is_nothrow(effects) ? Union{} : Any + return Any[exct] + end + excts = Any[] + tt = signature_type(f, types) + matches = _methods_by_ftype(tt, #=lim=#-1, world)::Vector + for match in matches + match = match::Core.MethodMatch + frame = Core.Compiler.typeinf_frame(interp, match, #=run_optimizer=#false) + if frame === nothing + exct = Any + else + exct = Core.Compiler.widenconst(frame.result.exc_result) + end + push!(excts, exct) + end + return excts +end + +_may_throw_methoderror(matches#=::Core.Compiler.MethodLookupResult=#) = + matches.ambig || !any(match::Core.MethodMatch->match.fully_covers, matches.matches) + +""" + Base.infer_exception_type( + f, types=default_tt(f); + world::UInt=get_world_counter(), + interp::Core.Compiler.AbstractInterpreter=Core.Compiler.NativeInterpreter(world)) -> exct::Type + +Returns the type of exception potentially thrown by the function call specified by `f` and `types`. + +# Arguments +- `f`: The function to analyze. +- `types` (optional): The argument types of the function. Defaults to the default tuple type of `f`. +- `world` (optional): The world counter to use for the analysis. Defaults to the current world counter. +- `interp` (optional): The abstract interpreter to use for the analysis. Defaults to a new `Core.Compiler.NativeInterpreter` with the specified `world`. + +# Returns +- `exct::Type`: The inferred type of exception that can be thrown by the function call + specified by the given call signature. -julia> effects = Base.infer_effects(foo, (Int,)) +!!! note + Note that, different from [`Base.infer_exception_types`](@ref), this doesn't give you the list + exception types for every possible matching method with the given `f` and `types`. + It provides a single exception type, taking into account all potential outcomes of + any function call entailed by the given signature type. + +# Example + +```julia +julia> f1(x) = x * 2; + +julia> Base.infer_exception_type(f1, (Int,)) +Union{} +``` + +The exception inferred as `Union{}` indicates that `f1(::Int)` will not throw any exception. + +```julia +julia> f2(x::Int) = x * 2; + +julia> Base.infer_exception_type(f2, (Integer,)) +MethodError +``` + +This case is pretty much the same as with `f1`, but there's a key difference to note. For +`f2`, the argument type is limited to `Int`, while the argument type is given as `Tuple{Integer}`. +Because of this, taking into account the chance of the method error entailed by the call +signature, the exception type is widened to `MethodError`. + +!!! warning + The `Base.infer_exception_type` function should not be used from generated functions; + doing so will result in an error. +""" +function infer_exception_type(@nospecialize(f), @nospecialize(types=default_tt(f)); + world::UInt=get_world_counter(), + interp::Core.Compiler.AbstractInterpreter=Core.Compiler.NativeInterpreter(world)) + check_generated_context(world) + if isa(f, Core.OpaqueClosure) + return Any # TODO + end + if isa(f, Core.Builtin) + effects = _builtin_effects(interp, f, types) + return Core.Compiler.is_nothrow(effects) ? Union{} : Any + end + tt = signature_type(f, types) + matches = Core.Compiler.findall(tt, Core.Compiler.method_table(interp)) + if matches === nothing + # unanalyzable call, i.e. the interpreter world might be newer than the world where + # the `f` is defined, return the unknown exception type + return Any + end + exct = Union{} + if _may_throw_methoderror(matches) + # account for the fact that we may encounter a MethodError with a non-covered or ambiguous signature. + exct = Core.Compiler.tmerge(exct, MethodError) + end + for match in matches.matches + match = match::Core.MethodMatch + frame = Core.Compiler.typeinf_frame(interp, match, #=run_optimizer=#false) + frame === nothing && return Any + exct = Core.Compiler.tmerge(exct, Core.Compiler.widenconst(frame.result.exc_result)) + end + return exct +end + +""" + Base.infer_effects( + f, types=default_tt(f); + world::UInt=get_world_counter(), + interp::Core.Compiler.AbstractInterpreter=Core.Compiler.NativeInterpreter(world)) -> effects::Effects + +Returns the possible computation effects of the function call specified by `f` and `types`. + +# Arguments +- `f`: The function to analyze. +- `types` (optional): The argument types of the function. Defaults to the default tuple type of `f`. +- `world` (optional): The world counter to use for the analysis. Defaults to the current world counter. +- `interp` (optional): The abstract interpreter to use for the analysis. Defaults to a new `Core.Compiler.NativeInterpreter` with the specified `world`. + +# Returns +- `effects::Effects`: The computed effects of the function call specified by the given call signature. + See the documentation of [`Effects`](@ref Core.Compiler.Effects) or [`Base.@assume_effects`](@ref) + for more information on the various effect properties. + +!!! note + Note that, different from [`Base.return_types`](@ref), this doesn't give you the list + effect analysis results for every possible matching method with the given `f` and `types`. + It provides a single effect, taking into account all potential outcomes of any function + call entailed by the given signature type. + +# Example + +```julia +julia> f1(x) = x * 2; + +julia> Base.infer_effects(f1, (Int,)) (+c,+e,+n,+t,+s,+m,+i) ``` -This function will return an `Effects` object with information about the computational effects of the function `foo` when called with an `Int` argument. See the documentation for `Effects` for more information on the various effect properties. +This function will return an `Effects` object with information about the computational +effects of the function `f1` when called with an `Int` argument. + +```julia +julia> f2(x::Int) = x * 2; + +julia> Base.infer_effects(f2, (Integer,)) +(+c,+e,!n,+t,+s,+m,+i) +``` + +This case is pretty much the same as with `f1`, but there's a key difference to note. For +`f2`, the argument type is limited to `Int`, while the argument type is given as `Tuple{Integer}`. +Because of this, taking into account the chance of the method error entailed by the call +signature, the `:nothrow` bit gets tainted. !!! warning - The `infer_effects` function should not be used from generated functions; + The `Base.infer_effects` function should not be used from generated functions; doing so will result in an error. # See Also @@ -1790,13 +2002,9 @@ This function will return an `Effects` object with information about the computa function infer_effects(@nospecialize(f), @nospecialize(types=default_tt(f)); world::UInt=get_world_counter(), interp::Core.Compiler.AbstractInterpreter=Core.Compiler.NativeInterpreter(world)) - (ccall(:jl_is_in_pure_context, Bool, ()) || world == typemax(UInt)) && - error("code reflection cannot be used from generated functions") + check_generated_context(world) if isa(f, Core.Builtin) - types = to_tuple_type(types) - argtypes = Any[types.parameters...] - rt = Core.Compiler.builtin_tfunction(interp, f, argtypes, nothing) - return Core.Compiler.builtin_effects(Core.Compiler.typeinf_lattice(interp), f, argtypes, rt) + return _builtin_effects(interp, f, types) end tt = signature_type(f, types) matches = Core.Compiler.findall(tt, Core.Compiler.method_table(interp)) @@ -1806,7 +2014,7 @@ function infer_effects(@nospecialize(f), @nospecialize(types=default_tt(f)); return Core.Compiler.Effects() end effects = Core.Compiler.EFFECTS_TOTAL - if matches.ambig || !any(match::Core.MethodMatch->match.fully_covers, matches.matches) + if _may_throw_methoderror(matches) # account for the fact that we may encounter a MethodError with a non-covered or ambiguous signature. effects = Core.Compiler.Effects(effects; nothrow=false) end diff --git a/test/reflection.jl b/test/reflection.jl index 7df6a76dfd0ff..87f16deacdded 100644 --- a/test/reflection.jl +++ b/test/reflection.jl @@ -1047,6 +1047,33 @@ ambig_effects_test(a, b) = 1 @test (Base.infer_effects(Core.Intrinsics.mul_int, ()); true) # `intrinsic_effects` shouldn't throw on empty `argtypes` end +@testset "infer_exception_type[s]" begin + # generic functions + @test Base.infer_exception_type(issue41694, (Int,)) == only(Base.infer_exception_types(issue41694, (Int,))) == ErrorException + @test Base.infer_exception_type((Int,)) do x + issue41694(x) + end == Base.infer_exception_types((Int,)) do x + issue41694(x) + end |> only == ErrorException + @test Base.infer_exception_type(issue41694) == only(Base.infer_exception_types(issue41694)) == ErrorException # use `default_tt` + let excts = Base.infer_exception_types(maybe_effectful, (Any,)) + @test any(==(Any), excts) + @test any(==(Union{}), excts) + end + @test Base.infer_exception_type(maybe_effectful, (Any,)) == Any + # `infer_exception_type` should account for MethodError + @test Base.infer_exception_type(issue41694, (Float64,)) == MethodError # definitive dispatch error + @test Base.infer_exception_type(issue41694, (Integer,)) == Union{MethodError,ErrorException} # possible dispatch error + @test Base.infer_exception_type(f_no_methods) == MethodError # no possible matching methods + @test Base.infer_exception_type(ambig_effects_test, (Int,Int)) == MethodError # ambiguity error + @test Base.infer_exception_type(ambig_effects_test, (Int,Any)) == MethodError # ambiguity error + # builtins + @test Base.infer_exception_type(typeof, (Any,)) === only(Base.infer_exception_types(typeof, (Any,))) === Union{} + @test Base.infer_exception_type(===, (Any,Any)) === only(Base.infer_exception_types(===, (Any,Any))) === Union{} + @test (Base.infer_exception_type(setfield!, ()); Base.infer_exception_types(setfield!, ()); true) # `infer_exception_type[s]` shouldn't throw on empty `argtypes` + @test (Base.infer_exception_type(Core.Intrinsics.mul_int, ()); Base.infer_exception_types(Core.Intrinsics.mul_int, ()); true) # `infer_exception_type[s]` shouldn't throw on empty `argtypes` +end + @test Base._methods_by_ftype(Tuple{}, -1, Base.get_world_counter()) == Any[] @test length(methods(Base.Broadcast.broadcasted, Tuple{Any, Any, Vararg})) > length(methods(Base.Broadcast.broadcasted, Tuple{Base.Broadcast.BroadcastStyle, Any, Vararg})) >= From da1258cb91a397dd4f1a31d2fd4af6fb354391bb Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Mon, 20 Nov 2023 18:16:44 +0900 Subject: [PATCH 2/5] `typeinf_local`: factor into `update_cycle_worklists!` utility --- base/compiler/abstractinterpretation.jl | 26 +++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 453b55caffa42..a811eafbfac02 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -3086,6 +3086,14 @@ function propagate_to_error_handler!(frame::InferenceState, currpc::Int, W::BitS end end +function update_cycle_worklists!(callback, frame::InferenceState) + for (caller, caller_pc) in frame.cycle_backedges + if callback(caller, caller_pc) + push!(caller.ip, block_for_inst(caller.cfg, caller_pc)) + end + end +end + # make as much progress on `frame` as possible (without handling cycles) function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) @assert !is_inferred(frame) @@ -3204,11 +3212,9 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) elseif isa(stmt, ReturnNode) rt = abstract_eval_value(interp, stmt.val, currstate, frame) if update_bestguess!(interp, frame, currstate, rt) - for (caller, caller_pc) in frame.cycle_backedges - if caller.ssavaluetypes[caller_pc] !== Any - # no reason to revisit if that call-site doesn't affect the final result - push!(caller.ip, block_for_inst(caller.cfg, caller_pc)) - end + update_cycle_worklists!(frame) do caller::InferenceState, caller_pc::Int + # no reason to revisit if that call-site doesn't affect the final result + return caller.ssavaluetypes[caller_pc] !== Any end end ssavaluetypes[frame.currpc] = Any @@ -3231,11 +3237,11 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) if cur_hand == 0 if !⊑(𝕃ₚ, exct, frame.exc_bestguess) frame.exc_bestguess = tmerge(𝕃ₚ, frame.exc_bestguess, exct) - for (caller, caller_pc) in frame.cycle_backedges - handler = caller.handler_at[caller_pc][1] - if (handler == 0 ? caller.exc_bestguess : caller.handlers[handler].exct) !== Any - push!(caller.ip, block_for_inst(caller.cfg, caller_pc)) - end + update_cycle_worklists!(frame) do caller::InferenceState, caller_pc::Int + caller_handler = caller.handler_at[caller_pc][1] + caller_exct = caller_handler == 0 ? + caller.exc_bestguess : caller.handlers[caller_handler].exct + return caller_exct !== Any end end else From 808d7ac7b8c52a1dc8eb6dc951376347f2c515c0 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Mon, 20 Nov 2023 18:17:13 +0900 Subject: [PATCH 3/5] inference: fix exception type of `typename` call --- base/compiler/abstractinterpretation.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index a811eafbfac02..b14ebe04796c6 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -2136,7 +2136,7 @@ function abstract_call_known(interp::AbstractInterpreter, @nospecialize(f), argtypes = Any[typeof(<:), argtypes[3], argtypes[2]] return abstract_call_known(interp, <:, ArgInfo(fargs, argtypes), si, sv, max_methods) elseif la == 2 && istopfunction(f, :typename) - return CallMeta(typename_static(argtypes[2]), Any, EFFECTS_TOTAL, MethodResultPure()) + return CallMeta(typename_static(argtypes[2]), Bottom, EFFECTS_TOTAL, MethodResultPure()) elseif f === Core._hasmethod return _hasmethod_tfunc(interp, argtypes, sv) end From d85cb0ec0b7cd1b2182612443624e8d2542eb8b9 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Mon, 20 Nov 2023 18:17:28 +0900 Subject: [PATCH 4/5] add missing type annotations --- base/compiler/ssair/irinterp.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/base/compiler/ssair/irinterp.jl b/base/compiler/ssair/irinterp.jl index 5b414f8786e98..b1c3fddc3df6e 100644 --- a/base/compiler/ssair/irinterp.jl +++ b/base/compiler/ssair/irinterp.jl @@ -51,7 +51,7 @@ function abstract_call(interp::AbstractInterpreter, arginfo::ArgInfo, irsv::IRIn return RTEffects(rt, exct, effects) end -function kill_block!(ir, bb) +function kill_block!(ir::IRCode, bb::Int) # Kill the entire block stmts = ir.cfg.blocks[bb].stmts for bidx = stmts @@ -64,7 +64,6 @@ function kill_block!(ir, bb) return end - function update_phi!(irsv::IRInterpretationState, from::Int, to::Int) ir = irsv.ir if length(ir.cfg.blocks[to].preds) == 0 From 8dd0cf5c43af7fe401157c6a5ff6704b71f8fd48 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Mon, 20 Nov 2023 18:19:27 +0900 Subject: [PATCH 5/5] inference: refine `exct` information if `:nothrow` is proven --- base/compiler/abstractinterpretation.jl | 9 ++++---- base/compiler/typeinfer.jl | 16 ++++++++++---- test/compiler/inference.jl | 29 +++++++++++++++++++++++++ 3 files changed, 46 insertions(+), 8 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index b14ebe04796c6..d99243a56e969 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -1196,13 +1196,14 @@ function semi_concrete_eval_call(interp::AbstractInterpreter, # state = InliningState(interp) # ir = ssa_inlining_pass!(irsv.ir, state, propagate_inbounds(irsv)) effects = result.effects - if !is_nothrow(effects) - effects = Effects(effects; nothrow) + if nothrow + effects = Effects(effects; nothrow=true) end if noub - effects = Effects(effects; noub = ALWAYS_TRUE) + effects = Effects(effects; noub=ALWAYS_TRUE) end - return ConstCallResults(rt, result.exct, SemiConcreteResult(mi, ir, effects), effects, mi) + exct = refine_exception_type(result.exct, effects) + return ConstCallResults(rt, exct, SemiConcreteResult(mi, ir, effects), effects, mi) end end end diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index 55026e54f777d..de296089f43be 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -503,6 +503,11 @@ function adjust_effects(sv::InferenceState) return ipo_effects end +function refine_exception_type(@nospecialize(exc_bestguess), ipo_effects::Effects) + ipo_effects.nothrow && return Bottom + return exc_bestguess +end + # inference completed on `me` # update the MethodInstance function finish(me::InferenceState, interp::AbstractInterpreter) @@ -539,8 +544,8 @@ function finish(me::InferenceState, interp::AbstractInterpreter) end me.result.valid_worlds = me.valid_worlds me.result.result = bestguess - me.result.ipo_effects = me.ipo_effects = adjust_effects(me) - me.result.exc_result = exc_bestguess + ipo_effects = me.result.ipo_effects = me.ipo_effects = adjust_effects(me) + me.result.exc_result = me.exc_bestguess = refine_exception_type(me.exc_bestguess, ipo_effects) if limited_ret # a parent may be cached still, but not this intermediate work: @@ -862,12 +867,13 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize isinferred = is_inferred(frame) edge = isinferred ? mi : nothing effects = isinferred ? frame.result.ipo_effects : adjust_effects(Effects(), method) # effects are adjusted already within `finish` for ipo_effects + exc_bestguess = refine_exception_type(frame.exc_bestguess, effects) # propagate newly inferred source to the inliner, allowing efficient inlining w/o deserialization: # note that this result is cached globally exclusively, we can use this local result destructively volatile_inf_result = isinferred && let inferred_src = result.src isa(inferred_src, CodeInfo) && (is_inlineable(inferred_src) || force_inline) end ? VolatileInferenceResult(result) : nothing - return EdgeCallResult(frame.bestguess, frame.exc_bestguess, edge, effects, volatile_inf_result) + return EdgeCallResult(frame.bestguess, exc_bestguess, edge, effects, volatile_inf_result) elseif frame === true # unresolvable cycle return EdgeCallResult(Any, Any, nothing, Effects()) @@ -875,7 +881,9 @@ function typeinf_edge(interp::AbstractInterpreter, method::Method, @nospecialize # return the current knowledge about this cycle frame = frame::InferenceState update_valid_age!(caller, frame.valid_worlds) - return EdgeCallResult(frame.bestguess, frame.exc_bestguess, nothing, adjust_effects(Effects(), method)) + effects = adjust_effects(Effects(), method) + exc_bestguess = refine_exception_type(frame.exc_bestguess, effects) + return EdgeCallResult(frame.bestguess, exc_bestguess, nothing, effects) end function cached_return_type(code::CodeInstance) diff --git a/test/compiler/inference.jl b/test/compiler/inference.jl index b75e5e5fe87f0..8cdee8baa90e6 100644 --- a/test/compiler/inference.jl +++ b/test/compiler/inference.jl @@ -5558,3 +5558,32 @@ function foo_typed_throw_metherr() return 1 end @test Base.return_types(foo_typed_throw_metherr) |> only === Float64 + +# using `exct` information if `:nothrow` is proven +Base.@assume_effects :nothrow function sin_nothrow(x::Float64) + x == Inf && return zero(x) + return sin(x) +end +@test Base.infer_exception_type(sin_nothrow, (Float64,)) == Union{} +@test Base.return_types((Float64,)) do x + try + return sin_nothrow(x) + catch err + return err + end +end |> only === Float64 +# for semi-concrete interpretation result too +Base.@constprop :aggressive function sin_maythrow(x::Float64, maythrow::Bool) + if maythrow + return sin(x) + else + return @noinline sin_nothrow(x) + end +end +@test Base.return_types((Float64,)) do x + try + return sin_maythrow(x, false) + catch err + return err + end +end |> only === Float64