Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/codeedges.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down
49 changes: 49 additions & 0 deletions test/codeedges.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading