Skip to content

Commit dde32b1

Browse files
authored
Use path-based control-flow analysis (#88)
This uses a more sophisticated approach to analyzing control flow, asking which paths include execution of the required statements and ensuring we mark those statements that affect critical branching. Fixes #87
1 parent b167804 commit dde32b1

File tree

2 files changed

+115
-34
lines changed

2 files changed

+115
-34
lines changed

src/codeedges.jl

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -609,6 +609,7 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
609609

610610
# Compute basic blocks, which we'll use to make sure we mark necessary control-flow
611611
cfg = Core.Compiler.compute_basic_blocks(src.code) # needed for control-flow analysis
612+
paths = enumerate_paths(cfg)
612613

613614
# We'll mostly use generic graph traversal to discover all the lines we need,
614615
# but structs are in a bit of a different category (especially on Julia 1.5+).
@@ -627,7 +628,8 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
627628
changed |= add_named_dependencies!(isrequired, edges, objs, norequire)
628629

629630
# Add control-flow
630-
changed |= add_control_flow!(isrequired, cfg, norequire)
631+
changed |= add_loops!(isrequired, cfg)
632+
changed |= add_control_flow!(isrequired, cfg, paths)
631633

632634
# So far, everything is generic graph traversal. Now we add some domain-specific information
633635
changed |= add_typedefs!(isrequired, src, edges, typedefs, norequire)
@@ -701,40 +703,93 @@ function add_obj!(isrequired, objs, obj, edges::CodeEdges, norequire)
701703
return chngd
702704
end
703705

704-
# Add control-flow. For any basic block with an evaluated statement inside it,
705-
# check to see if the block has any successors, and if so mark that block's exit statement.
706-
# Likewise, any preceding blocks should have *their* exit statement marked.
707-
function add_control_flow!(isrequired, cfg, norequire)
706+
## Add control-flow
707+
708+
struct Path
709+
path::Vector{Int}
710+
visited::BitSet
711+
end
712+
Path() = Path(Int[], BitSet())
713+
Path(i::Int) = Path([i], BitSet([i]))
714+
Path(path::Path) = copy(path)
715+
Base.copy(path::Path) = Path(copy(path.path), copy(path.visited))
716+
Base.in(node::Int, path::Path) = node path.visited
717+
Base.push!(path::Path, node::Int) = (push!(path.path, node); push!(path.visited, node); return path)
718+
719+
# Mark loops that contain evaluated statements
720+
function add_loops!(isrequired, cfg)
708721
changed = false
722+
for (ibb, bb) in enumerate(cfg.blocks)
723+
needed = false
724+
for ibbp in bb.preds
725+
# Is there a backwards-pointing predecessor, and if so are there any required statements between the two?
726+
ibbp > ibb || continue # not a loop-block predecessor
727+
r, rp = rng(bb), rng(cfg.blocks[ibbp])
728+
r = first(r):first(rp)-1
729+
needed |= any(view(isrequired, r))
730+
end
731+
if needed
732+
# Mark the final statement of all predecessors
733+
for ibbp in bb.preds
734+
rp = rng(cfg.blocks[ibbp])
735+
changed |= !isrequired[last(rp)]
736+
isrequired[last(rp)] = true
737+
end
738+
end
739+
end
740+
return changed
741+
end
742+
743+
enumerate_paths(cfg) = enumerate_paths!(Path[], cfg, Path(1))
744+
function enumerate_paths!(paths, cfg, path)
745+
bb = cfg.blocks[path.path[end]]
746+
if isempty(bb.succs)
747+
push!(paths, copy(path))
748+
return paths
749+
end
750+
for ibbs in bb.succs
751+
if ibbs path
752+
push!(paths, push!(copy(path), ibbs)) # close the loop
753+
continue
754+
end
755+
enumerate_paths!(paths, cfg, push!(copy(path), ibbs))
756+
end
757+
return paths
758+
end
759+
760+
# Mark exits of blocks that bifurcate execution paths in ways that matter for required statements
761+
function add_control_flow!(isrequired, cfg, paths::AbstractVector{Path})
762+
withnode, withoutnode, shared = BitSet(), BitSet(), BitSet()
763+
changed, _changed = false, true
709764
blocks = cfg.blocks
710765
nblocks = length(blocks)
711-
_changed = true
712766
while _changed
713767
_changed = false
714768
for (ibb, bb) in enumerate(blocks)
715769
r = rng(bb)
716770
if any(view(isrequired, r))
717-
if ibb != nblocks
771+
# Check if the exit of this block is a GotoNode
772+
if length(bb.succs) == 1
718773
idxlast = r[end]
719-
idxlast norequire && continue
720774
_changed |= !isrequired[idxlast]
721775
isrequired[idxlast] = true
722776
end
723-
for ibbp in bb.preds
724-
ibbp > 0 || continue # see Core.Compiler.compute_basic_blocks, near comment re :enter
725-
rpred = rng(blocks[ibbp])
726-
idxlast = rpred[end]
727-
idxlast norequire && continue
728-
_changed |= !isrequired[idxlast]
729-
isrequired[idxlast] = true
777+
empty!(withnode)
778+
empty!(withoutnode)
779+
for path in paths
780+
union!(ibb path ? withnode : withoutnode, path.visited)
730781
end
731-
for ibbs in bb.succs
732-
ibbs == nblocks && continue
733-
rpred = rng(blocks[ibbs])
734-
idxlast = rpred[end]
735-
idxlast norequire && continue
736-
_changed |= !isrequired[idxlast]
737-
isrequired[idxlast] = true
782+
empty!(shared)
783+
union!(shared, withnode)
784+
intersect!(shared, withoutnode)
785+
for icfbb in shared
786+
cfbb = blocks[icfbb]
787+
if any((shared), cfbb.succs)
788+
rcfbb = rng(blocks[icfbb])
789+
idxlast = rcfbb[end]
790+
_changed |= !isrequired[idxlast]
791+
isrequired[idxlast] = true
792+
end
738793
end
739794
end
740795
end
@@ -850,7 +905,7 @@ function selective_eval!(@nospecialize(recurse), frame::Frame, isrequired::Abstr
850905
pcexec = (pcexec === nothing ? pclast : pcexec)::Int
851906
frame.pc = pcexec
852907
node = pc_expr(frame)
853-
is_return(node) && return lookup_return(frame, node)
908+
is_return(node) && return isrequired[pcexec] ? lookup_return(frame, node) : nothing
854909
isassigned(frame.framedata.ssavalues, pcexec) && return frame.framedata.ssavalues[pcexec]
855910
return nothing
856911
end

test/codeedges.jl

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ module ModSelective end
6565
# Check that the result of direct evaluation agrees with selective evaluation
6666
Core.eval(ModEval, ex)
6767
isrequired = lines_required(:x, src, edges)
68-
@test sum(isrequired) == 1 + isdefined(Core, :get_binding_type) * 3 # get_binding_type + convert + typeassert
68+
# theere is too much diversity in lowering across Julia versions to make it useful to test `sum(isrequired)`
6969
selective_eval_fromstart!(frame, isrequired)
7070
@test ModSelective.x === ModEval.x
7171
@test allmissing(ModSelective, (:y, :z, :a, :b, :k))
@@ -128,6 +128,23 @@ module ModSelective end
128128
Core.eval(ModEval, ex)
129129
@test ModSelective.a3 === ModEval.a3 == 2
130130
@test allmissing(ModSelective, (:z3, :x3, :y3))
131+
# ensure we mark all needed control-flow for loops and conditionals,
132+
# and don't fall-through incorrectly
133+
ex = quote
134+
valcf = 0
135+
for i = 1:5
136+
global valcf
137+
if valcf < 4
138+
valcf += 1
139+
end
140+
end
141+
end
142+
frame = Frame(ModSelective, ex)
143+
src = frame.framecode.src
144+
edges = CodeEdges(src)
145+
isrequired = lines_required(:valcf, src, edges)
146+
selective_eval_fromstart!(frame, isrequired)
147+
@test ModSelective.valcf == 4
131148

132149
ex = quote
133150
if Sys.iswindows()
@@ -143,7 +160,7 @@ module ModSelective end
143160
src = frame.framecode.src
144161
edges = CodeEdges(src)
145162
isrequired = lines_required(:c_os, src, edges)
146-
@test sum(isrequired) >= length(isrequired) - 2
163+
@test sum(isrequired) >= length(isrequired) - 3
147164
selective_eval_fromstart!(frame, isrequired)
148165
Core.eval(ModEval, ex)
149166
@test ModSelective.c_os === ModEval.c_os == Sys.iswindows()
@@ -359,9 +376,12 @@ module ModSelective end
359376
str = String(take!(io))
360377
@test occursin(r"slot 1:\n preds: ssas: \[\d+, \d+\], slots: ∅, names: ∅;\n succs: ssas: \[\d+, \d+, \d+\], slots: ∅, names: ∅;\n assign @: \[\d+, \d+\]", str)
361378
@test occursin(r"succs: ssas: ∅, slots: \[\d+\], names: ∅;", str)
362-
@test occursin(r"s:\n preds: ssas: \[\d+\], slots: ∅, names: ∅;\n succs: ssas: \[\d+, \d+, \d+\], slots: ∅, names: ∅;\n assign @: \[\d, \d+\]", str) ||
363-
occursin(r"s:\n preds: ssas: \[\d+, \d+\], slots: ∅, names: ∅;\n succs: ssas: \[\d+, \d+, \d+\], slots: ∅, names: ∅;\n assign @: \[\d, \d+\]", str) # with global var inference
364-
if Base.VERSION < v"1.8" # changed by global var inference
379+
# Some of these differ due to changes by Julia version in global var inference
380+
if Base.VERSION < v"1.10"
381+
@test occursin(r"s:\n preds: ssas: \[\d+\], slots: ∅, names: ∅;\n succs: ssas: \[\d+, \d+, \d+\], slots: ∅, names: ∅;\n assign @: \[\d, \d+\]", str) ||
382+
occursin(r"s:\n preds: ssas: \[\d+, \d+\], slots: ∅, names: ∅;\n succs: ssas: \[\d+, \d+, \d+\], slots: ∅, names: ∅;\n assign @: \[\d, \d+\]", str) # with global var inference
383+
end
384+
if Base.VERSION < v"1.8"
365385
@test occursin(r"\d+ preds: ssas: \[\d+\], slots: ∅, names: \[:s\];\n\d+ succs: ssas: ∅, slots: ∅, names: \[:s\];", str)
366386
end
367387
LoweredCodeUtils.print_with_code(io, src, cl)
@@ -380,17 +400,21 @@ module ModSelective end
380400
edges = CodeEdges(src)
381401
show(io, edges)
382402
str = String(take!(io))
383-
@test occursin(r"s: assigned on \[\d, \d+\], depends on \[\d+\], and used by \[\d+, \d+, \d+\]", str) ||
384-
occursin(r"s: assigned on \[\d, \d+\], depends on \[\d+, \d+\], and used by \[\d+, \d+, \d+\]", str) # global var inference
403+
if Base.VERSION < v"1.10"
404+
@test occursin(r"s: assigned on \[\d, \d+\], depends on \[\d+\], and used by \[\d+, \d+, \d+\]", str) ||
405+
occursin(r"s: assigned on \[\d, \d+\], depends on \[\d+, \d+\], and used by \[\d+, \d+, \d+\]", str) # global var inference
406+
end
385407
if Base.VERSION < v"1.9"
386408
@test (count(occursin("statement $i depends on [1, $(i-1), $(i+1)] and is used by [1, $(i+1)]", str) for i = 1:length(src.code)) == 1) ||
387409
(count(occursin("statement $i depends on [4, $(i-1), $(i+4)] and is used by [$(i+2)]", str) for i = 1:length(src.code)) == 1)
388410
end
389411
LoweredCodeUtils.print_with_code(io, src, edges)
390412
str = String(take!(io))
391413
if isdefined(Base.IRShow, :show_ir_stmt)
392-
@test occursin(r"s: assigned on \[\d, \d+\], depends on \[\d+\], and used by \[\d+, \d+, \d+\]", str) ||
393-
occursin(r"s: assigned on \[\d, \d+\], depends on \[\d+, \d+\], and used by \[\d+, \d+, \d+\]", str)
414+
if Base.VERSION < v"1.10"
415+
@test occursin(r"s: assigned on \[\d, \d+\], depends on \[\d+\], and used by \[\d+, \d+, \d+\]", str) ||
416+
occursin(r"s: assigned on \[\d, \d+\], depends on \[\d+, \d+\], and used by \[\d+, \d+, \d+\]", str)
417+
end
394418
if Base.VERSION < v"1.9"
395419
@test (count(occursin("preds: [1, $(i-1), $(i+1)], succs: [1, $(i+1)]", str) for i = 1:length(src.code)) == 1) ||
396420
(count(occursin("preds: [4, $(i-1), $(i+4)], succs: [$(i+2)]", str) for i = 1:length(src.code)) == 1) # global var inference
@@ -404,8 +428,10 @@ module ModSelective end
404428
LoweredCodeUtils.print_with_code(io, frame, edges)
405429
str = String(take!(io))
406430
if isdefined(Base.IRShow, :show_ir_stmt)
407-
@test occursin(r"s: assigned on \[\d, \d+\], depends on \[\d+\], and used by \[\d+, \d+, \d+\]", str) ||
408-
occursin(r"s: assigned on \[\d, \d+\], depends on \[\d, \d+\], and used by \[\d+, \d+, \d+\]", str) # global var inference
431+
if Base.VERSION < v"1.10"
432+
@test occursin(r"s: assigned on \[\d, \d+\], depends on \[\d+\], and used by \[\d+, \d+, \d+\]", str) ||
433+
occursin(r"s: assigned on \[\d, \d+\], depends on \[\d, \d+\], and used by \[\d+, \d+, \d+\]", str) # global var inference
434+
end
409435
if Base.VERSION < v"1.9"
410436
@test (count(occursin("preds: [1, $(i-1), $(i+1)], succs: [1, $(i+1)]", str) for i = 1:length(src.code)) == 1) ||
411437
(count(occursin("preds: [4, $(i-1), $(i+4)], succs: [$(i+2)]", str) for i = 1:length(src.code)) == 1) # global var inference

0 commit comments

Comments
 (0)