diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index fe94a7f0600aaa..7c3510263ca426 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -619,7 +619,7 @@ struct MethodCallResult end end -function is_all_const_arg((; argtypes)::ArgInfo) +function is_all_const_arg((; fargs, argtypes)::ArgInfo) for a in argtypes if !isa(a, Const) && !isconstType(a) && !issingletontype(a) return false @@ -628,8 +628,9 @@ function is_all_const_arg((; argtypes)::ArgInfo) return true end -function concrete_eval_const_proven_total_or_error(interp::AbstractInterpreter, - @nospecialize(f), (; argtypes)::ArgInfo, _::InferenceState) +function concrete_eval_const_proven_total_or_error( + interp::AbstractInterpreter, + @nospecialize(f), argtypes::Vector{Any}) args = Any[ (a = widenconditional(argtypes[i]); isa(a, Const) ? a.val : isconstType(a) ? (a::DataType).parameters[1] : @@ -672,7 +673,7 @@ function abstract_call_method_with_const_args(interp::AbstractInterpreter, resul return nothing end if f !== nothing && result.edge !== nothing && is_total_or_error(result.edge_effects) && is_all_const_arg(arginfo) - rt = concrete_eval_const_proven_total_or_error(interp, f, arginfo, sv) + rt = concrete_eval_const_proven_total_or_error(interp, f, arginfo.argtypes) add_backedge!(result.edge, sv) if rt === nothing # The evaulation threw. By :consistent-cy, we're guaranteed this would have happened at runtime diff --git a/base/compiler/ssair/inlining.jl b/base/compiler/ssair/inlining.jl index d3a322f7d44c27..a14a23326d58ea 100644 --- a/base/compiler/ssair/inlining.jl +++ b/base/compiler/ssair/inlining.jl @@ -1034,22 +1034,18 @@ function inline_invoke!( # TODO: We could union split out the signature check and continue on return nothing end + argtypes = invoke_rewrite(sig.argtypes) result = info.result - if isa(result, ConstResult) - item = const_result_item(result, state) - else - argtypes = invoke_rewrite(sig.argtypes) - if isa(result, InferenceResult) - (; mi) = item = InliningTodo(result, argtypes) - validate_sparams(mi.sparam_vals) || return nothing - if argtypes_to_type(argtypes) <: mi.def.sig - state.mi_cache !== nothing && (item = resolve_todo(item, state, flag)) - handle_single_case!(ir, idx, stmt, item, todo, state.params, true) - return nothing - end + if isa(result, InferenceResult) + (; mi) = item = InliningTodo(result, argtypes) + validate_sparams(mi.sparam_vals) || return nothing + if argtypes_to_type(argtypes) <: mi.def.sig + state.mi_cache !== nothing && (item = resolve_todo(item, state, flag)) + handle_single_case!(ir, idx, stmt, item, todo, state.params, true) + return nothing end - item = analyze_method!(match, argtypes, flag, state) end + item = analyze_method!(match, argtypes, flag, state) handle_single_case!(ir, idx, stmt, item, todo, state.params, true) return nothing end @@ -1245,18 +1241,28 @@ function handle_const_call!( for match in meth j += 1 result = results[j] + if result === false + # Inference determined that this call is guaranteed to throw. + # Do not inline. + fully_covered = false + continue + end if isa(result, ConstResult) - case = const_result_item(result, state) + if !isdefined(result, :result) || !is_inlineable_constant(result.result) + case = compileable_specialization(state.et, result.mi, EFFECTS_TOTAL) + else + case = ConstantCase(quoted(result.result)) + end signature_union = Union{signature_union, result.mi.specTypes} push!(cases, InliningCase(result.mi.specTypes, case)) continue - elseif isa(result, InferenceResult) - signature_union = Union{signature_union, result.linfo.specTypes} - fully_covered &= handle_inf_result!(result, argtypes, flag, state, cases) - else - @assert result === nothing + end + if result === nothing signature_union = Union{signature_union, match.spec_types} fully_covered &= handle_match!(match, argtypes, flag, state, cases) + else + signature_union = Union{signature_union, result.linfo.specTypes} + fully_covered &= handle_const_result!(result, argtypes, flag, state, cases) end end end @@ -1290,7 +1296,7 @@ function handle_match!( return true end -function handle_inf_result!( +function handle_const_result!( result::InferenceResult, argtypes::Vector{Any}, flag::UInt8, state::InliningState, cases::Vector{InliningCase}) (; mi) = item = InliningTodo(result, argtypes) @@ -1303,14 +1309,6 @@ function handle_inf_result!( return true end -function const_result_item(result::ConstResult, state::InliningState) - if !isdefined(result, :result) || !is_inlineable_constant(result.result) - return compileable_specialization(state.et, result.mi, EFFECTS_TOTAL) - else - return ConstantCase(quoted(result.result)) - end -end - function handle_cases!(ir::IRCode, idx::Int, stmt::Expr, @nospecialize(atype), cases::Vector{InliningCase}, fully_covered::Bool, todo::Vector{Pair{Int, Any}}, params::OptimizationParams) @@ -1377,11 +1375,7 @@ function assemble_inline_todo!(ir::IRCode, state::InliningState) ir, idx, stmt, result, flag, sig, state, todo) else - if isa(result, ConstResult) - item = const_result_item(result, state) - else - item = analyze_method!(info.match, sig.argtypes, flag, state) - end + item = analyze_method!(info.match, sig.argtypes, flag, state) handle_single_case!(ir, idx, stmt, item, todo, state.params) end continue diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index 03ba383de4f610..a15f81abde9195 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -277,8 +277,9 @@ function _typeinf(interp::AbstractInterpreter, frame::InferenceState) return true end -function CodeInstance( - result::InferenceResult, @nospecialize(inferred_result), valid_worlds::WorldRange) +function CodeInstance(result::InferenceResult, @nospecialize(inferred_result), + valid_worlds::WorldRange, effects::Effects, ipo_effects::Effects, + relocatability::UInt8) local const_flags::Int32 result_type = result.result @assert !(result_type isa LimitedAccuracy) @@ -308,13 +309,10 @@ function CodeInstance( const_flags = 0x00 end end - relocatability = isa(inferred_result, Vector{UInt8}) ? inferred_result[end] : UInt8(0) return CodeInstance(result.linfo, widenconst(result_type), rettype_const, inferred_result, const_flags, first(valid_worlds), last(valid_worlds), - # TODO: Actually do something with non-IPO effects - encode_effects(result.ipo_effects), encode_effects(result.ipo_effects), - relocatability) + encode_effects(effects), encode_effects(ipo_effects), relocatability) end # For the NativeInterpreter, we don't need to do an actual cache query to know @@ -388,7 +386,10 @@ function cache_result!(interp::AbstractInterpreter, result::InferenceResult) # TODO: also don't store inferred code if we've previously decided to interpret this function if !already_inferred inferred_result = transform_result_for_cache(interp, linfo, valid_worlds, result.src) - code_cache(interp)[linfo] = CodeInstance(result, inferred_result, valid_worlds) + relocatability = isa(inferred_result, Vector{UInt8}) ? inferred_result[end] : UInt8(0) + code_cache(interp)[linfo] = CodeInstance(result, inferred_result, valid_worlds, + # TODO: Actually do something with non-IPO effects + result.ipo_effects, result.ipo_effects, relocatability) end unlock_mi_inference(interp, linfo) nothing diff --git a/base/expr.jl b/base/expr.jl index 38e89d284c989f..59e6b075d8db99 100644 --- a/base/expr.jl +++ b/base/expr.jl @@ -378,10 +378,9 @@ end `@assume_effects` overrides the compiler's effect modeling for the given method. `ex` must be a method definition or `@ccall` expression. -!!! warning - Improper use of this macro causes undefined behavior (including crashes, - incorrect answers, or other hard to track bugs). Use with care and only if - absolutely required. +WARNING: Improper use of this macro causes undefined behavior (including crashes, +incorrect answers, or other hard to track bugs). Use with care and only if absolutely +required. In general, each `setting` value makes an assertion about the behavior of the function, without requiring the compiler to prove that this behavior is indeed diff --git a/test/compiler/inline.jl b/test/compiler/inline.jl index 4261665edc80eb..f2130e1c7eab4a 100644 --- a/test/compiler/inline.jl +++ b/test/compiler/inline.jl @@ -4,8 +4,6 @@ using Test using Base.Meta using Core: ReturnNode -include(normpath(@__DIR__, "irutils.jl")) - """ Helper to walk the AST and call a function on every node. """ @@ -152,6 +150,19 @@ end @test !any(x -> x isa Expr && x.head === :invoke, src.code) end +function fully_eliminated(f, args) + @nospecialize f args + let code = code_typed(f, args)[1][1].code + return length(code) == 1 && isa(code[1], ReturnNode) + end +end +function fully_eliminated(f, args, retval) + @nospecialize f args + let code = code_typed(f, args)[1][1].code + return length(code) == 1 && isa(code[1], ReturnNode) && code[1].val == retval + end +end + # check that ismutabletype(type) can be fully eliminated f_mutable_nothrow(s::String) = Val{typeof(s).name.flags} @test fully_eliminated(f_mutable_nothrow, (String,)) @@ -235,7 +246,7 @@ function f_subtype() T = SomeArbitraryStruct T <: Bool end -@test fully_eliminated(f_subtype, Tuple{}; retval=false) +@test fully_eliminated(f_subtype, Tuple{}, false) # check that pointerref gets deleted if unused f_pointerref(T::Type{S}) where S = Val(length(T.parameters)) @@ -259,7 +270,7 @@ function foo_apply_apply_type_svec() B = Tuple{Float32, Float32} Core.apply_type(A..., B.types...) end -@test fully_eliminated(foo_apply_apply_type_svec, Tuple{}; retval=NTuple{3, Float32}) +@test fully_eliminated(foo_apply_apply_type_svec, Tuple{}, NTuple{3, Float32}) # The that inlining doesn't drop ambiguity errors (#30118) c30118(::Tuple{Ref{<:Type}, Vararg}) = nothing @@ -273,7 +284,7 @@ b30118(x...) = c30118(x) f34900(x::Int, y) = x f34900(x, y::Int) = y f34900(x::Int, y::Int) = invoke(f34900, Tuple{Int, Any}, x, y) -@test fully_eliminated(f34900, Tuple{Int, Int}; retval=Core.Argument(2)) +@test fully_eliminated(f34900, Tuple{Int, Int}, Core.Argument(2)) @testset "check jl_ir_flag_inlineable for inline macro" begin @test ccall(:jl_ir_flag_inlineable, Bool, (Any,), first(methods(@inline x -> x)).source) @@ -313,7 +324,10 @@ struct NonIsBitsDims dims::NTuple{N, Int} where N end NonIsBitsDims() = NonIsBitsDims(()) -@test fully_eliminated(NonIsBitsDims, (); retval=QuoteNode(NonIsBitsDims())) +let ci = code_typed(NonIsBitsDims, Tuple{})[1].first + @test length(ci.code) == 1 && isa(ci.code[1], ReturnNode) && + ci.code[1].val.value == NonIsBitsDims() +end struct NonIsBitsDimsUndef dims::NTuple{N, Int} where N @@ -909,7 +923,7 @@ end g() = Core.get_binding_type($m, :y) end - @test fully_eliminated(m.f, Tuple{}; retval=Int) + @test fully_eliminated(m.f, Tuple{}, Int) src = code_typed(m.g, ())[][1] @test count(iscall((src, Core.get_binding_type)), src.code) == 1 @test m.g() === Any @@ -948,48 +962,17 @@ end @test fully_eliminated(f_sin_perf, Tuple{}) # Test that we inline the constructor of something that is not const-inlineable -const THE_REF_NULL = Ref{Int}() const THE_REF = Ref{Int}(0) struct FooTheRef x::Ref - FooTheRef(v) = new(v === nothing ? THE_REF_NULL : THE_REF) -end -let src = code_typed1() do - FooTheRef(nothing) - end - @test count(isnew, src.code) == 1 -end -let src = code_typed1() do - FooTheRef(0) - end - @test count(isnew, src.code) == 1 + FooTheRef() = new(THE_REF) end -let src = code_typed1() do - Base.@invoke FooTheRef(nothing::Any) - end - @test count(isnew, src.code) == 1 -end -let src = code_typed1() do - Base.@invoke FooTheRef(0::Any) - end - @test count(isnew, src.code) == 1 -end -@test fully_eliminated() do - FooTheRef(nothing) - nothing -end -@test fully_eliminated() do - FooTheRef(0) - nothing -end -@test fully_eliminated() do - Base.@invoke FooTheRef(nothing::Any) - nothing -end -@test fully_eliminated() do - Base.@invoke FooTheRef(0::Any) - nothing +f_make_the_ref() = FooTheRef() +f_make_the_ref_but_dont_return_it() = (FooTheRef(); nothing) +let src = code_typed1(f_make_the_ref, ()) + @test count(x->isexpr(x, :new), src.code) == 1 end +@test fully_eliminated(f_make_the_ref_but_dont_return_it, Tuple{}) # Test that the Core._apply_iterate bail path taints effects function f_apply_bail(f) diff --git a/test/compiler/irpasses.jl b/test/compiler/irpasses.jl index 128fd6cc84b7b2..9eb77490cbdb43 100644 --- a/test/compiler/irpasses.jl +++ b/test/compiler/irpasses.jl @@ -4,7 +4,31 @@ using Test using Base.Meta using Core: PhiNode, SSAValue, GotoNode, PiNode, QuoteNode, ReturnNode, GotoIfNot -include(normpath(@__DIR__, "irutils.jl")) +# utilities +# ========= + +import Core.Compiler: argextype, singleton_type + +argextype(@nospecialize args...) = argextype(args..., Any[]) +code_typed1(args...; kwargs...) = first(only(code_typed(args...; kwargs...)))::Core.CodeInfo +get_code(args...; kwargs...) = code_typed1(args...; kwargs...).code + +# check if `x` is a statement with a given `head` +isnew(@nospecialize x) = Meta.isexpr(x, :new) + +# check if `x` is a dynamic call of a given function +iscall(y) = @nospecialize(x) -> iscall(y, x) +function iscall((src, f)::Tuple{Core.CodeInfo,Base.Callable}, @nospecialize(x)) + return iscall(x) do @nospecialize x + singleton_type(argextype(x, src)) === f + end +end +iscall(pred::Base.Callable, @nospecialize(x)) = Meta.isexpr(x, :call) && pred(x.args[1]) + +# check if `x` is a statically-resolved call of a function whose name is `sym` +isinvoke(y) = @nospecialize(x) -> isinvoke(y, x) +isinvoke(sym::Symbol, @nospecialize(x)) = isinvoke(mi->mi.def.name===sym, x) +isinvoke(pred::Function, @nospecialize(x)) = Meta.isexpr(x, :invoke) && pred(x.args[1]::Core.MethodInstance) # domsort # ======= @@ -449,7 +473,9 @@ struct FooPartial global f_partial f_partial(x) = new(x, 2).x end -@test fully_eliminated(f_partial, Tuple{Float64}) +let ci = code_typed(f_partial, Tuple{Float64})[1].first + @test length(ci.code) == 1 && isa(ci.code[1], ReturnNode) +end # A SSAValue after the compaction line let m = Meta.@lower 1 + 1 @@ -631,7 +657,11 @@ function no_op_refint(r) r[] return end -@test fully_eliminated(no_op_refint,Tuple{Base.RefValue{Int}}; retval=nothing) +let code = code_typed(no_op_refint,Tuple{Base.RefValue{Int}})[1].first.code + @test length(code) == 1 + @test isa(code[1], Core.ReturnNode) + @test code[1].val === nothing +end # check getfield elim handling of GlobalRef const _some_coeffs = (1,[2],3,4) @@ -743,6 +773,19 @@ end # test `stmt_effect_free` and DCE # =============================== +function fully_eliminated(f, args) + @nospecialize f args + let code = code_typed(f, args)[1][1].code + return length(code) == 1 && isa(code[1], ReturnNode) + end +end +function fully_eliminated(f, args, retval) + @nospecialize f args + let code = code_typed(f, args)[1][1].code + return length(code) == 1 && isa(code[1], ReturnNode) && code[1].val == retval + end +end + let # effect-freeness computation for array allocation # should eliminate dead allocations diff --git a/test/compiler/irutils.jl b/test/compiler/irutils.jl deleted file mode 100644 index 06d261720bdf82..00000000000000 --- a/test/compiler/irutils.jl +++ /dev/null @@ -1,34 +0,0 @@ -import Core: CodeInfo, ReturnNode, MethodInstance -import Core.Compiler: argextype, singleton_type -import Base.Meta: isexpr - -argextype(@nospecialize args...) = argextype(args..., Any[]) -code_typed1(args...; kwargs...) = first(only(code_typed(args...; kwargs...)))::CodeInfo -get_code(args...; kwargs...) = code_typed1(args...; kwargs...).code - -# check if `x` is a statement with a given `head` -isnew(@nospecialize x) = isexpr(x, :new) -isreturn(@nospecialize x) = isa(x, ReturnNode) - -# check if `x` is a dynamic call of a given function -iscall(y) = @nospecialize(x) -> iscall(y, x) -function iscall((src, f)::Tuple{CodeInfo,Base.Callable}, @nospecialize(x)) - return iscall(x) do @nospecialize x - singleton_type(argextype(x, src)) === f - end -end -iscall(pred::Base.Callable, @nospecialize(x)) = isexpr(x, :call) && pred(x.args[1]) - -# check if `x` is a statically-resolved call of a function whose name is `sym` -isinvoke(y) = @nospecialize(x) -> isinvoke(y, x) -isinvoke(sym::Symbol, @nospecialize(x)) = isinvoke(mi->mi.def.name===sym, x) -isinvoke(pred::Function, @nospecialize(x)) = isexpr(x, :invoke) && pred(x.args[1]::MethodInstance) - -function fully_eliminated(@nospecialize args...; retval=(@__FILE__), kwargs...) - code = code_typed1(args...; kwargs...).code - if retval !== (@__FILE__) - return length(code) == 1 && isreturn(code[1]) && code[1].val == retval - else - return length(code) == 1 && isreturn(code[1]) - end -end