@@ -1091,16 +1091,32 @@ end
10911091
10921092# select statements that should be concretized, and actually interpreted rather than abstracted
10931093function select_statements (mod:: Module , src:: CodeInfo )
1094- stmts = src. code
10951094 cl = LoweredCodeUtils. CodeLinks (mod, src) # make `CodeEdges` hold `CodeLinks`?
10961095 edges = LoweredCodeUtils. CodeEdges (src, cl)
1097-
1098- concretize = falses (length (stmts))
1099-
1100- select_direct_requirement! (concretize, stmts, edges)
1101-
1096+ concretize = falses (length (src. code))
1097+ select_direct_requirement! (concretize, src. code, edges)
11021098 select_dependencies! (concretize, src, edges, cl)
1099+ return concretize
1100+ end
11031101
1102+ # just for testing, and debugging
1103+ function select_statements (mod:: Module , src:: CodeInfo , names:: Symbol... )
1104+ cl = LoweredCodeUtils. CodeLinks (mod, src) # make `CodeEdges` hold `CodeLinks`?
1105+ edges = LoweredCodeUtils. CodeEdges (src, cl)
1106+ concretize = falses (length (src. code))
1107+ objs = Set {GlobalRef} (GlobalRef (mod, name) for name in names)
1108+ LoweredCodeUtils. add_requests! (concretize, objs, edges, ())
1109+ select_dependencies! (concretize, src, edges, cl)
1110+ return concretize
1111+ end
1112+ function select_statements (mod:: Module , src:: CodeInfo , idxs:: Int... )
1113+ cl = LoweredCodeUtils. CodeLinks (mod, src) # make `CodeEdges` hold `CodeLinks`?
1114+ edges = LoweredCodeUtils. CodeEdges (src, cl)
1115+ concretize = falses (length (src. code))
1116+ for idx = idxs
1117+ concretize[idx] |= true
1118+ end
1119+ select_dependencies! (concretize, src, edges, cl)
11041120 return concretize
11051121end
11061122
@@ -1173,67 +1189,70 @@ end
11731189
11741190# The goal of this function is to request concretization of the minimal necessary control
11751191# flow to evaluate statements whose concretization have already been requested.
1176- # The basic approach is to check if there are any active successors for each basic block,
1177- # and if there is an active successor and the terminator is not a fall-through, then request
1178- # the concretization of that terminator. Additionally, for conditional terminators, a simple
1179- # optimization using post-domination analysis is also performed.
1180- function add_control_flow! (concretize:: BitVector , src:: CodeInfo , cfg:: CFG , postdomtree)
1192+ # The basic algorithm is based on what was proposed in [^Wei84]. If there is even one active
1193+ # block in the blocks reachable from a conditional branch up to its successors' nearest
1194+ # common post-dominator (referred to as **INFL** in the paper), it is necessary to follow
1195+ # that conditional branch and execute the code. Otherwise, execution can be short-circuited
1196+ # from the conditional branch to the nearest common post-dominator.
1197+ #
1198+ # COMBAK: It is important to note that in Julia's intermediate code representation (`CodeInfo`),
1199+ # "short-circuiting" a specific code region is not a simple task. Simply ignoring the path
1200+ # to the post-dominator does not guarantee fall-through to the post-dominator. Therefore,
1201+ # a more careful implementation is required for this aspect.
1202+ #
1203+ # [Wei84]: M. Weiser, "Program Slicing," IEEE Transactions on Software Engineering, 10, pages 352-357, July 1984.
1204+ function add_control_flow! (concretize:: BitVector , src:: CodeInfo , cfg:: CFG , domtree, postdomtree)
11811205 local changed:: Bool = false
11821206 function mark_concretize! (idx:: Int )
11831207 if ! concretize[idx]
1184- concretize[idx] = true
1208+ changed |= concretize[idx] = true
11851209 return true
11861210 end
11871211 return false
11881212 end
1189- nblocks = length (cfg. blocks)
1190- for bbidx = 1 : nblocks
1191- bb = cfg. blocks[bbidx] # forward traversal
1213+ for bbidx = 1 : length (cfg. blocks) # forward traversal
1214+ bb = cfg. blocks[bbidx]
11921215 nsuccs = length (bb. succs)
11931216 if nsuccs == 0
11941217 continue
11951218 elseif nsuccs == 1
1196- terminator_idx = bb. stmts[end ]
1197- if src. code[terminator_idx] isa GotoNode
1198- # If the destination of this `GotoNode` is not active, it's fine to ignore
1199- # the control flow caused by this `GotoNode` and treat it as a fall-through.
1200- # If the block that is fallen through to is active and has a dependency on
1201- # this goto block, then the concretization of this goto block should already
1202- # be requested (at some point of the higher concretization convergence cycle
1203- # of `select_dependencies`), and thus, this `GotoNode` will be concretized.
1204- if any (@view concretize[cfg. blocks[only (bb. succs)]. stmts])
1205- changed |= mark_concretize! (terminator_idx)
1219+ termidx = bb. stmts[end ]
1220+ if src. code[termidx] isa GotoNode
1221+ succ = only (bb. succs)
1222+ if any (@view concretize[cfg. blocks[succ]. stmts])
1223+ dominator = nearest_common_dominator (domtree, bbidx, succ)
1224+ if dominator ≠ succ
1225+ for blk in reachable_blocks (cfg, dominator, succ)
1226+ if blk == dominator || blk == succ
1227+ continue
1228+ end
1229+ if any (@view concretize[cfg. blocks[blk]. stmts])
1230+ mark_concretize! (termidx)
1231+ break
1232+ end
1233+ end
1234+ else
1235+ mark_concretize! (termidx)
1236+ end
12061237 end
12071238 end
1239+ continue # we can just fall-through
12081240 elseif nsuccs == 2
1209- terminator_idx = bb. stmts[end ]
1210- @assert is_conditional_terminator (src. code[terminator_idx ]) " invalid IR"
1241+ termidx = bb. stmts[end ]
1242+ @assert is_conditional_terminator (src. code[termidx ]) " invalid IR"
12111243 succ1, succ2 = bb. succs
1212- succ1_req = any (@view concretize[cfg. blocks[succ1]. stmts])
1213- succ2_req = any (@view concretize[cfg. blocks[succ2]. stmts])
1214- if succ1_req
1215- if succ2_req
1216- changed |= mark_concretize! (terminator_idx)
1217- else
1218- active_bb, inactive_bb = succ1, succ2
1219- @goto asymmetric_case
1244+ postdominator = nearest_common_dominator (postdomtree, succ1, succ2)
1245+ inflblks = reachable_blocks (cfg, succ1, postdominator) ∪ reachable_blocks (cfg, succ2, postdominator)
1246+ for blk in inflblks
1247+ if blk == postdominator
1248+ continue
12201249 end
1221- elseif succ2_req
1222- active_bb, inactive_bb = succ2, succ1
1223- @label asymmetric_case
1224- # We can ignore the control flow of this conditional terminator and treat
1225- # it as a fall-through if only one of its successors is active and the
1226- # active block post-dominates the inactive one, since the post-domination
1227- # ensures that the active basic block will be reached regardless of the
1228- # control flow.
1229- if CC. postdominates (postdomtree, active_bb, inactive_bb)
1230- # fall through this block
1231- else
1232- changed |= mark_concretize! (terminator_idx)
1250+ if any (@view concretize[cfg. blocks[blk]. stmts])
1251+ mark_concretize! (termidx)
1252+ break
12331253 end
1234- else
1235- # both successors are inactive, just fall through this block
12361254 end
1255+ # we can just fall-through to the post dominator block (by ignoring all statements between)
12371256 end
12381257 end
12391258 return changed
@@ -1242,6 +1261,25 @@ end
12421261is_conditional_terminator (@nospecialize stmt) = stmt isa GotoIfNot ||
12431262 (@static @isdefined (EnterNode) ? stmt isa EnterNode : isexpr (stmt, :enter ))
12441263
1264+ function reachable_blocks (cfg:: CFG , from_bb:: Int , to_bb:: Int )
1265+ worklist = Int[from_bb]
1266+ visited = BitSet (from_bb)
1267+ if to_bb == from_bb
1268+ return visited
1269+ end
1270+ push! (visited, to_bb)
1271+ function visit! (bb:: Int )
1272+ if bb ∉ visited
1273+ push! (visited, bb)
1274+ push! (worklist, bb)
1275+ end
1276+ end
1277+ while ! isempty (worklist)
1278+ foreach (visit!, cfg. blocks[pop! (worklist)]. succs)
1279+ end
1280+ return visited
1281+ end
1282+
12451283function add_required_inplace! (concretize:: BitVector , src:: CodeInfo , edges, cl)
12461284 changed = false
12471285 for i = 1 : length (src. code)
@@ -1272,27 +1310,42 @@ function is_arg_requested(@nospecialize(arg), concretize, edges, cl)
12721310 return false
12731311end
12741312
1313+ # The purpose of this function is to find other statements that affect the execution of the
1314+ # statements choosen by `select_direct_dependencies!`. In other words, it extracts the
1315+ # minimal amount of code necessary to realize the required concretization.
1316+ # This technique is generally referred to as "program slicing," and specifically as
1317+ # "static program slicing". The basic algorithm implemented here is an extension of the one
1318+ # proposed in https://dl.acm.org/doi/10.5555/800078.802557, which is especially tuned for
1319+ # Julia's intermediate code representation.
12751320function select_dependencies! (concretize:: BitVector , src:: CodeInfo , edges, cl)
12761321 typedefs = LoweredCodeUtils. find_typedefs (src)
1277- cfg = CC. compute_basic_blocks (src. code)
1278- postdomtree = CC. construct_postdomtree (cfg. blocks)
1322+ cfg = compute_basic_blocks (src. code)
1323+ domtree = construct_domtree (cfg. blocks)
1324+ postdomtree = construct_postdomtree (cfg. blocks)
12791325
12801326 while true
12811327 changed = false
12821328
1283- # discover struct/method definitions at the beginning,
1284- # and propagate the definition requirements by tracking SSA precedessors
1329+ # Discover Dtruct/method definitions at the beginning,
1330+ # and propagate the definition requirements by tracking SSA precedessors.
1331+ # (TODO maybe hoist this out of the loop?)
12851332 changed |= LoweredCodeUtils. add_typedefs! (concretize, src, edges, typedefs, ())
12861333 changed |= add_ssa_preds! (concretize, src, edges, ())
12871334
1288- # mark some common inplace operations like `push!(x, ...)` and `setindex!(x, ...)`
1289- # when `x` has been marked already: otherwise we may end up using it with invalid state
1335+ # Mark some common inplace operations like `push!(x, ...)` and `setindex!(x, ...)`
1336+ # when `x` has been marked already: otherwise we may end up using it with invalid state.
1337+ # However, note that this is an incomplete approach, and note that the slice created
1338+ # by this routine will not be sound because of this. This is because
1339+ # `add_required_inplace!` only requires certain special-cased function calls and
1340+ # does not take into account the possibility that arguments may be mutated in
1341+ # arbitrary function calls. Ideally, function calls should be required while
1342+ # considering the effects of these statements, or by some sort of an
1343+ # inter-procedural program slicing
12901344 changed |= add_required_inplace! (concretize, src, edges, cl)
12911345 changed |= add_ssa_preds! (concretize, src, edges, ())
12921346
1293- # mark necessary control flows,
1294- # and propagate the definition requirements by tracking SSA precedessors
1295- changed |= add_control_flow! (concretize, src, cfg, postdomtree)
1347+ # Mark necessary control flows.
1348+ changed |= add_control_flow! (concretize, src, cfg, domtree, postdomtree)
12961349 changed |= add_ssa_preds! (concretize, src, edges, ())
12971350
12981351 changed || break
0 commit comments