diff --git a/src/codeedges.jl b/src/codeedges.jl index 417e558..1f6bde3 100644 --- a/src/codeedges.jl +++ b/src/codeedges.jl @@ -765,6 +765,7 @@ function lines_required!( # Add control-flow changed |= add_control_flow!(isrequired, src, cfg, postdomtree) + changed |= add_handler_exits!(isrequired, src, edges, norequire) # So far, everything is generic graph traversal. Now we add some domain-specific information changed |= add_typedefs!(isrequired, src, edges, typedefs, norequire) @@ -900,8 +901,33 @@ function add_control_flow!(isrequired, src::CodeInfo, cfg::CFG, postdomtree) return changed[] end -is_conditional_terminator(@nospecialize stmt) = stmt isa GotoIfNot || - (@static @isdefined(EnterNode) ? stmt isa EnterNode : isexpr(stmt, :enter)) +is_conditional_terminator(@nospecialize stmt) = stmt isa GotoIfNot || is_enter(stmt) + +is_enter(@nospecialize stmt) = + @static @isdefined(EnterNode) ? stmt isa EnterNode : isexpr(stmt, :enter) + +# A required `EnterNode` enters an exception handler, and possibly a dynamic scope, which +# only the `:leave`s referencing it exit, so the statements after its `try` block would run +# within them otherwise. Likewise, the `:pop_exception`s referencing it restore the +# exception stack after its `catch` block. On Julia 1.10, `:leave`s count the handlers to +# exit instead of referencing them, so only the `:pop_exception`s are added. +function add_handler_exits!(isrequired, src::CodeInfo, edges::CodeEdges, norequire) + changed = false + for idx = 1:length(src.code) + stmt = src.code[idx] + isrequired[idx] && is_enter(stmt) || continue + for succ in edges.succs[idx] + isrequired[succ] && continue + succ ∈ norequire && continue + succstmt = src.code[succ] + if isexpr(succstmt, :leave) || isexpr(succstmt, :pop_exception) + isrequired[succ] = true + changed = true + end + end + end + return changed +end function is_conditional_block_active(isrequired, bb::BasicBlock, cfg::CFG, postdomtree) return visit_𝑰𝑵𝑭𝑳_blocks(bb, cfg, postdomtree) do postdominator::Int, 𝑰𝑵𝑭𝑳::BitSet diff --git a/test/codeedges.jl b/test/codeedges.jl index c66c7e6..75a188a 100644 --- a/test/codeedges.jl +++ b/test/codeedges.jl @@ -744,4 +744,53 @@ end end end +@static if isdefined(Core.IR, :EnterNode) + @testset "required `EnterNode`s are exited" begin + let mod = Module(:ModExitScope) + Core.eval(mod, :(const sv = Base.ScopedValues.ScopedValue(0))) + src = Meta.lower(mod, quote + Base.ScopedValues.@with sv => 1 begin + global inside = sv[] + end + global outside = sv[] + end).args[1] + edges = CodeEdges(mod, src) + controller = SelectiveEvalController() + objs = Set((GlobalRef(mod, :inside), GlobalRef(mod, :outside))) + isrequired = lines_required!(falses(length(src.code)), objs, src, edges, controller) + selective_eval_fromstart!( + LoweredCodeUtils.RecursiveInterpreter(), Frame(mod, src), + isrequired, controller, true) + @test @invokelatest(mod.inside) == 1 + @test @invokelatest(mod.outside) == 0 + end + + # A handler left entered would catch the error of `y`, and run it again + let mod = Module(:ModExitHandler) + Core.eval(mod, quote + hits = Symbol[] + boom() = (push!(hits, :boom); error("boom")) + end) + src = Meta.lower(mod, quote + try + global x = 1 + catch + global caught = true + end + global y = boom() + end).args[1] + edges = CodeEdges(mod, src) + controller = SelectiveEvalController() + objs = Set((GlobalRef(mod, :x), GlobalRef(mod, :y))) + isrequired = lines_required!(falses(length(src.code)), objs, src, edges, controller) + @test_throws ErrorException selective_eval_fromstart!( + LoweredCodeUtils.RecursiveInterpreter(), Frame(mod, src), + isrequired, controller, true) + @test @invokelatest(mod.x) == 1 + @test @invokelatest(mod.hits) == [:boom] + @test !@invokelatest(isdefined(mod, :caught)) + end + end +end + end # module codeedges