From 0044c538e7b7498180d42365810bbad32a14c431 Mon Sep 17 00:00:00 2001 From: Shuhei Kadowaki Date: Wed, 4 May 2022 19:28:30 +0900 Subject: [PATCH] refactor unreachability analysis --- base/compiler/abstractinterpretation.jl | 19 ++- base/compiler/inferencestate.jl | 6 +- base/compiler/optimize.jl | 103 ++++++++++----- base/compiler/typeinfer.jl | 160 +++++++----------------- base/compiler/typelattice.jl | 23 +++- base/compiler/utilities.jl | 1 + test/compiler/ssair.jl | 22 ++++ 7 files changed, 172 insertions(+), 162 deletions(-) diff --git a/base/compiler/abstractinterpretation.jl b/base/compiler/abstractinterpretation.jl index 36ab6b81f47a0..e4c7e4a28e3a1 100644 --- a/base/compiler/abstractinterpretation.jl +++ b/base/compiler/abstractinterpretation.jl @@ -1835,7 +1835,13 @@ function abstract_eval_special_value(interp::AbstractInterpreter, @nospecialize( elseif isa(e, SSAValue) return abstract_eval_ssavalue(e, sv) elseif isa(e, SlotNumber) || isa(e, Argument) - return vtypes[slot_id(e)].typ + sn = slot_id(e) + s = vtypes[sn] + if s.undef === true || # may not be defined + s.typ === Bottom # never analyzed + sv.src.slotflags[sn] |= SLOT_USEDUNDEF | SLOT_STATICUNDEF + end + return s.typ elseif isa(e, GlobalRef) return abstract_eval_global(e.mod, e.name, sv) end @@ -2022,10 +2028,11 @@ function abstract_eval_statement(interp::AbstractInterpreter, @nospecialize(e), sym = e.args[1] t = Bool if isa(sym, SlotNumber) - vtyp = vtypes[slot_id(sym)] + sn = slot_id(sym) + vtyp = vtypes[sn] if vtyp.typ === Bottom t = Const(false) # never assigned previously - elseif !vtyp.undef + elseif vtyp.undef === false t = Const(true) # definitely assigned previously end elseif isa(sym, Symbol) @@ -2332,9 +2339,9 @@ function typeinf_local(interp::AbstractInterpreter, frame::InferenceState) if isa(fname, SlotNumber) changes = StateUpdate(fname, VarState(Any, false), changes, false) end - elseif hd === :code_coverage_effect || - (hd !== :boundscheck && # :boundscheck can be narrowed to Bool - hd !== nothing && is_meta_expr_head(hd)) + elseif hd === :code_coverage_effect || (hd !== nothing && + hd !== :boundscheck && # :boundscheck can be narrowed to Bool + is_meta_expr_head(hd)) # these do not generate code else t = abstract_eval_statement(interp, stmt, changes, frame) diff --git a/base/compiler/inferencestate.jl b/base/compiler/inferencestate.jl index 24423deef8623..3bc28026a27be 100644 --- a/base/compiler/inferencestate.jl +++ b/base/compiler/inferencestate.jl @@ -4,8 +4,8 @@ # (only used in abstractinterpret, doesn't appear in optimize) struct VarState typ - undef::Bool - VarState(@nospecialize(typ), undef::Bool) = new(typ, undef) + undef::Union{Nothing,Bool} # nothing if unanalyzed + VarState(@nospecialize(typ), undef::Union{Nothing,Bool}) = new(typ, undef) end """ @@ -152,7 +152,7 @@ mutable struct InferenceState stmt_types[1] = stmt_type1 = VarTable(undef, nslots) for i in 1:nslots argtyp = (i > nargs) ? Bottom : argtypes[i] - stmt_type1[i] = VarState(argtyp, i > nargs) + stmt_type1[i] = VarState(argtyp, i > nargs && nothing) slottypes[i] = argtyp end diff --git a/base/compiler/optimize.jl b/base/compiler/optimize.jl index 195e8c39f9af0..956743d73ff55 100644 --- a/base/compiler/optimize.jl +++ b/base/compiler/optimize.jl @@ -91,19 +91,26 @@ mutable struct OptimizationState linfo::MethodInstance src::CodeInfo ir::Union{Nothing, IRCode} + was_reached::Union{Nothing, BitSet} stmt_info::Vector{Any} mod::Module sptypes::Vector{Any} # static parameters slottypes::Vector{Any} inlining::InliningState function OptimizationState(frame::InferenceState, params::OptimizationParams, interp::AbstractInterpreter) + was_reached = BitSet() + for i = 1:length(frame.stmt_types) + if isa(frame.stmt_types[i], VarTable) + push!(was_reached, i) + end + end s_edges = frame.stmt_edges[1]::Vector{Any} inlining = InliningState(params, EdgeTracker(s_edges, frame.valid_worlds), WorldView(code_cache(interp), frame.world), interp) return new(frame.linfo, - frame.src, nothing, frame.stmt_info, frame.mod, + frame.src, nothing, was_reached, frame.stmt_info, frame.mod, frame.sptypes, frame.slottypes, inlining) end function OptimizationState(linfo::MethodInstance, src::CodeInfo, params::OptimizationParams, interp::AbstractInterpreter) @@ -131,11 +138,13 @@ mutable struct OptimizationState WorldView(code_cache(interp), get_world_counter()), interp) return new(linfo, - src, nothing, stmt_info, mod, + src, nothing, nothing, stmt_info, mod, sptypes_from_meth_instance(linfo), slottypes, inlining) end end +was_reached((; was_reached)::OptimizationState, pc::Int) = was_reached === nothing || pc in was_reached + function OptimizationState(linfo::MethodInstance, params::OptimizationParams, interp::AbstractInterpreter) src = retrieve_code_info(linfo) src === nothing && return nothing @@ -554,21 +563,13 @@ function run_passes(ci::CodeInfo, sv::OptimizationState, caller::InferenceResult end function convert_to_ircode(ci::CodeInfo, sv::OptimizationState) - code = copy_exprargs(ci.code) coverage = coverage_enabled(sv.mod) - # Go through and add an unreachable node after every - # Union{} call. Then reindex labels. - idx = 1 - oldidx = 1 - changemap = fill(0, length(code)) - prevloc = zero(eltype(ci.codelocs)) - stmtinfo = sv.stmt_info - codelocs = ci.codelocs - ssavaluetypes = ci.ssavaluetypes::Vector{Any} - ssaflags = ci.ssaflags + linetable = ci.linetable + if !isa(linetable, Vector{LineInfoNode}) + linetable = collect(LineInfoNode, linetable::Vector{Any})::Vector{LineInfoNode} + end if !coverage && JLOptions().code_coverage == 3 # path-specific coverage mode - for line in ci.linetable - line = line::LineInfoNode + for line in linetable if is_file_tracked(line.file) # if any line falls in a tracked file enable coverage for all coverage = true @@ -576,8 +577,39 @@ function convert_to_ircode(ci::CodeInfo, sv::OptimizationState) end end end + # Go through and add an unreachable node after every + # Union{} call. Then reindex labels + code = copy_exprargs(ci.code) + stmtinfo = sv.stmt_info + codelocs = ci.codelocs + ssavaluetypes = ci.ssavaluetypes::Vector{Any} + ssaflags = ci.ssaflags + meta = Expr[] + idx = 1 + oldidx = 1 + changemap = fill(0, length(code)) labelmap = coverage ? fill(0, length(code)) : changemap + prevloc = zero(eltype(ci.codelocs)) while idx <= length(code) + stmt = code[idx] + if process_meta!(meta, stmt) || !(is_meta_expr(stmt) || was_reached(sv, oldidx)) + if oldidx < length(labelmap) + changemap[oldidx] != 0 && (changemap[oldidx+1] = changemap[oldidx]) + if coverage && labelmap[oldidx] != 0 + labelmap[oldidx + 1] = labelmap[oldidx] + end + changemap[oldidx] = -1 + coverage && (labelmap[oldidx] = -1) + end + # TODO: It would be more efficient to do this in bulk + deleteat!(code, idx) + deleteat!(codelocs, idx) + deleteat!(ssavaluetypes, idx) + deleteat!(stmtinfo, idx) + deleteat!(ssaflags, idx) + oldidx += 1 + continue + end codeloc = codelocs[idx] if coverage && codeloc != prevloc && codeloc != 0 # insert a side-effect instruction before the current instruction in the same basic block @@ -593,7 +625,16 @@ function convert_to_ircode(ci::CodeInfo, sv::OptimizationState) idx += 1 prevloc = codeloc end - if code[idx] isa Expr && ssavaluetypes[idx] === Union{} + if isa(stmt, GotoIfNot) + # replace GotoIfNot with: + # - GotoNode if the fallthrough target is unreachable + # - no-op if the branch target is unreachable + if !was_reached(sv, oldidx + 1) + code[idx] = GotoNode(stmt.dest) + elseif !was_reached(sv, stmt.dest) + code[idx] = nothing + end + elseif stmt isa Expr && ssavaluetypes[idx] === Union{} if !(idx < length(code) && isa(code[idx + 1], ReturnNode) && !isdefined((code[idx + 1]::ReturnNode), :val)) # insert unreachable in the same basic block after the current instruction (splitting it) insert!(code, idx + 1, ReturnNode()) @@ -611,28 +652,22 @@ function convert_to_ircode(ci::CodeInfo, sv::OptimizationState) idx += 1 oldidx += 1 end + renumber_ir_elements!(code, changemap, labelmap) - meta = Expr[] - for i = 1:length(code) - code[i] = process_meta!(meta, code[i]) - end strip_trailing_junk!(ci, code, stmtinfo) - cfg = compute_basic_blocks(code) types = Any[] stmts = InstructionStream(code, types, stmtinfo, codelocs, ssaflags) - linetable = ci.linetable - isa(linetable, Vector{LineInfoNode}) || (linetable = collect(LineInfoNode, linetable::Vector{Any})) - ir = IRCode(stmts, cfg, linetable, sv.slottypes, meta, sv.sptypes) - return ir + cfg = compute_basic_blocks(code) + return IRCode(stmts, cfg, linetable, sv.slottypes, meta, sv.sptypes) end function process_meta!(meta::Vector{Expr}, @nospecialize stmt) if isexpr(stmt, :meta) && length(stmt.args) ≥ 1 push!(meta, stmt) - return nothing + return true end - return stmt + return false end function slot2reg(ir::IRCode, ci::CodeInfo, sv::OptimizationState) @@ -794,7 +829,9 @@ end function cumsum_ssamap!(ssamap::Vector{Int}) rel_change = 0 + any_change = false for i = 1:length(ssamap) + any_change = any_change || ssamap[i] != 0 rel_change += ssamap[i] if ssamap[i] == -1 # Keep a marker that this statement was deleted @@ -803,16 +840,15 @@ function cumsum_ssamap!(ssamap::Vector{Int}) ssamap[i] = rel_change end end + return any_change end function renumber_ir_elements!(body::Vector{Any}, ssachangemap::Vector{Int}, labelchangemap::Vector{Int}) - cumsum_ssamap!(labelchangemap) + any_change = cumsum_ssamap!(labelchangemap) if ssachangemap !== labelchangemap - cumsum_ssamap!(ssachangemap) - end - if labelchangemap[end] == 0 && ssachangemap[end] == 0 - return + any_change |= cumsum_ssamap!(ssachangemap) end + any_change || return for i = 1:length(body) el = body[i] if isa(el, GotoNode) @@ -822,7 +858,8 @@ function renumber_ir_elements!(body::Vector{Any}, ssachangemap::Vector{Int}, lab if isa(cond, SSAValue) cond = SSAValue(cond.id + ssachangemap[cond.id]) end - body[i] = GotoIfNot(cond, el.dest + labelchangemap[el.dest]) + was_deleted = labelchangemap[el.dest] == typemin(Int) + body[i] = was_deleted ? cond : GotoIfNot(cond, el.dest + labelchangemap[el.dest]) elseif isa(el, ReturnNode) if isdefined(el, :val) val = el.val diff --git a/base/compiler/typeinfer.jl b/base/compiler/typeinfer.jl index c76849d599c46..cd4bea410ad0c 100644 --- a/base/compiler/typeinfer.jl +++ b/base/compiler/typeinfer.jl @@ -507,7 +507,7 @@ function finish(me::InferenceState, interp::AbstractInterpreter) # annotate fulltree with type information, # either because we are the outermost code, or we might use this later doopt = (me.cached || me.parent !== nothing) - type_annotate!(me, doopt) + type_annotate!(me) if doopt && may_optimize(interp) me.result.src = OptimizationState(me, OptimizationParams(interp), interp) else @@ -565,49 +565,6 @@ function widen_all_consts!(src::CodeInfo) return src end -function annotate_slot_load!(e::Expr, vtypes::VarTable, sv::InferenceState, undefs::Array{Bool,1}) - head = e.head - i0 = 1 - if is_meta_expr_head(head) || head === :const - return - end - if head === :(=) || head === :method - i0 = 2 - end - for i = i0:length(e.args) - subex = e.args[i] - if isa(subex, Expr) - annotate_slot_load!(subex, vtypes, sv, undefs) - elseif isa(subex, SlotNumber) - e.args[i] = visit_slot_load!(subex, vtypes, sv, undefs) - end - end -end - -function annotate_slot_load(@nospecialize(e), vtypes::VarTable, sv::InferenceState, undefs::Array{Bool,1}) - if isa(e, Expr) - annotate_slot_load!(e, vtypes, sv, undefs) - elseif isa(e, SlotNumber) - return visit_slot_load!(e, vtypes, sv, undefs) - end - return e -end - -function visit_slot_load!(sl::SlotNumber, vtypes::VarTable, sv::InferenceState, undefs::Array{Bool,1}) - id = slot_id(sl) - s = vtypes[id] - vt = widenconditional(ignorelimited(s.typ)) - if s.undef - # find used-undef variables - undefs[id] = true - end - # add type annotations where needed - if !(sv.slottypes[id] ⊑ vt) - return TypedSlot(id, vt) - end - return sl -end - function record_slot_assign!(sv::InferenceState) # look at all assignments to slots # and union the set of types stored there @@ -618,9 +575,9 @@ function record_slot_assign!(sv::InferenceState) ssavaluetypes = sv.src.ssavaluetypes::Vector{Any} for i = 1:length(body) expr = body[i] - st_i = states[i] + state = states[i] # find all reachable assignments to locals - if isa(st_i, VarTable) && isa(expr, Expr) && expr.head === :(=) + if isa(state, VarTable) && isa(expr, Expr) && expr.head === :(=) lhs = expr.args[1] rhs = expr.args[2] if isa(lhs, SlotNumber) @@ -641,11 +598,42 @@ function record_slot_assign!(sv::InferenceState) end end -# annotate types of all symbols in AST -function type_annotate!(sv::InferenceState, run_optimizer::Bool) - # as an optimization, we delete dead statements immediately if we're going to run the optimizer - # (otherwise, we'll perhaps run the optimization passes later, outside of inference) +function annotate_slots!(sv::InferenceState) + states = sv.stmt_types + body = sv.src.code::Vector{Any} + slottypes = sv.slottypes::Vector{Any} + for i = 1:length(body) + state = states[i] + if isa(state, VarTable) + body[i] = annotate_slot(slottypes, state, body[i]) + end + end +end + +function annotate_slot(slottypes::Vector{Any}, vtypes::VarTable, @nospecialize x) + if isa(x, Expr) + head = x.head + i0 = (head === :(=) || head === :method) ? 2 : 1 + for i = i0:length(x.args) + x.args[i] = annotate_slot(slottypes, vtypes, x.args[i]) + end + return x + elseif isa(x, ReturnNode) && isdefined(x, :val) + return ReturnNode(annotate_slot(slottypes, vtypes, x.val)) + elseif isa(x, GotoIfNot) + return GotoIfNot(annotate_slot(slottypes, vtypes, x.cond), x.dest) + elseif isa(x, SlotNumber) + id = slot_id(x) + typ = widenconditional(ignorelimited(vtypes[id].typ)) + if !(slottypes[id] ⊑ typ) + return TypedSlot(id, typ) + end + end + return x +end +# annotate types of all symbols in AST +function type_annotate!(sv::InferenceState) # remove all unused ssa values src = sv.src ssavaluetypes = src.ssavaluetypes::Vector{Any} @@ -661,75 +649,11 @@ function type_annotate!(sv::InferenceState, run_optimizer::Bool) @assert !(sv.bestguess isa LimitedAccuracy) sv.src.rettype = sv.bestguess - # annotate variables load types - # remove dead code optimization - # and compute which variables may be used undef - states = sv.stmt_types - nslots = length(states[1]::VarTable) - undefs = fill(false, nslots) - body = src.code::Array{Any,1} - nexpr = length(body) - - # replace GotoIfNot with its condition if the branch target is unreachable - for i = 1:nexpr - expr = body[i] - if isa(expr, GotoIfNot) - if !isa(states[expr.dest], VarTable) - body[i] = Expr(:call, GlobalRef(Core, :typeassert), expr.cond, GlobalRef(Core, :Bool)) - end - end - end - - i = 1 - oldidx = 0 - changemap = fill(0, nexpr) - - while i <= nexpr - oldidx += 1 - st_i = states[i] - expr = body[i] - if isa(st_i, VarTable) - # st_i === nothing => unreached statement (see issue #7836) - if isa(expr, Expr) - annotate_slot_load!(expr, st_i, sv, undefs) - elseif isa(expr, ReturnNode) && isdefined(expr, :val) - body[i] = ReturnNode(annotate_slot_load(expr.val, st_i, sv, undefs)) - elseif isa(expr, GotoIfNot) - body[i] = GotoIfNot(annotate_slot_load(expr.cond, st_i, sv, undefs), expr.dest) - elseif isa(expr, SlotNumber) - body[i] = visit_slot_load!(expr, st_i, sv, undefs) - end - else - if isa(expr, Expr) && is_meta_expr_head(expr.head) - # keep any lexically scoped expressions - elseif run_optimizer - deleteat!(body, i) - deleteat!(states, i) - deleteat!(ssavaluetypes, i) - deleteat!(src.codelocs, i) - deleteat!(sv.stmt_info, i) - deleteat!(src.ssaflags, i) - nexpr -= 1 - changemap[oldidx] = -1 - continue - else - body[i] = Const(expr) # annotate that this statement actually is dead - end - end - i += 1 - end - - if run_optimizer - renumber_ir_elements!(body, changemap) - end + # TODO move this to convert_to_ircode (https://github.com/JuliaLang/julia/pull/43999) + # add type annotations where needed + annotate_slots!(sv) - # finish marking used-undef variables - for j = 1:nslots - if undefs[j] - src.slotflags[j] |= SLOT_USEDUNDEF | SLOT_STATICUNDEF - end - end - nothing + return nothing end # at the end, all items in b's cycle diff --git a/base/compiler/typelattice.jl b/base/compiler/typelattice.jl index 79db3b6cf20b6..1949b856ca9d3 100644 --- a/base/compiler/typelattice.jl +++ b/base/compiler/typelattice.jl @@ -342,7 +342,17 @@ widenconst(t::TypeVar) = error("unhandled TypeVar") widenconst(t::TypeofVararg) = error("unhandled Vararg") widenconst(t::LimitedAccuracy) = error("unhandled LimitedAccuracy") -issubstate(a::VarState, b::VarState) = (a.typ ⊑ b.typ && a.undef <= b.undef) +function issubstate(a::VarState, b::VarState) + a.typ ⊑ b.typ || return false + aundef, bundef = a.undef, b.undef + if aundef === nothing + return true + elseif bundef === nothing + return false + else + return aundef <= bundef + end +end function smerge(sa::Union{NotFound,VarState}, sb::Union{NotFound,VarState}) sa === sb && return sa @@ -350,7 +360,16 @@ function smerge(sa::Union{NotFound,VarState}, sb::Union{NotFound,VarState}) sb === NOT_FOUND && return sa issubstate(sa, sb) && return sb issubstate(sb, sa) && return sa - return VarState(tmerge(sa.typ, sb.typ), sa.undef | sb.undef) + t = tmerge(sa.typ, sb.typ) + aundef, bundef = sa.undef, sb.undef + if aundef === nothing + undef = bundef + elseif bundef === nothing + undef = aundef + else + undef = aundef | bundef + end + return VarState(t, undef) end @inline tchanged(@nospecialize(n), @nospecialize(o)) = o === NOT_FOUND || (n !== NOT_FOUND && !(n ⊑ o)) diff --git a/base/compiler/utilities.jl b/base/compiler/utilities.jl index 07281a353dbb6..fe97b81c07e24 100644 --- a/base/compiler/utilities.jl +++ b/base/compiler/utilities.jl @@ -64,6 +64,7 @@ end # Meta expression head, these generally can't be deleted even when they are # in a dead branch but can be ignored when analyzing uses/liveness. is_meta_expr_head(head::Symbol) = head === :boundscheck || head === :meta || head === :loopinfo +is_meta_expr(@nospecialize x) = isa(x, Expr) && is_meta_expr_head(x.head) sym_isless(a::Symbol, b::Symbol) = ccall(:strcmp, Int32, (Ptr{UInt8}, Ptr{UInt8}), a, b) < 0 diff --git a/test/compiler/ssair.jl b/test/compiler/ssair.jl index 0328d78652e6f..cee0af6eb0992 100644 --- a/test/compiler/ssair.jl +++ b/test/compiler/ssair.jl @@ -334,3 +334,25 @@ f_if_typecheck() = (if nothing; end; unsafe_load(Ptr{Int}(0))) stderr = IOBuffer() success(pipeline(Cmd(cmd); stdout=stdout, stderr=stderr)) && isempty(String(take!(stderr))) end + +let src = code_typed() do + local a + try + a = a + 1 + catch err + @isdefined a + end + end |> only |> first + @test any(src.code) do @nospecialize x + Meta.isexpr(x, :throw_undef_if_not) && x.args[1] === :a + end +end +let src = code_typed() do + a = @isdefined x + x = 42 + b = @isdefined x + a, b + end |> only |> first + ret = (only(src.code)::Core.ReturnNode).val + @test ret === (false, true) +end