@@ -182,6 +182,42 @@ function postprint_linelinks(io::IO, idx::Int, src::CodeInfo, cl::CodeLinks, bbc
182182 return nothing
183183end
184184
185+ struct CFGShortCut
186+ from:: Int # pc of GotoIfNot with inactive 𝑰𝑵𝑭𝑳 blocks
187+ to:: Int # pc of the entry of the nearest common post-dominator of the GotoIfNot's successors
188+ end
189+
190+ """
191+ controller::SelectiveEvalController
192+
193+ When this object is passed as the `recurse` argument of `selective_eval!`,
194+ the selective execution is adjusted as follows:
195+
196+ - **Implicit return**: In Julia's IR representation (`CodeInfo`), the final block does not
197+ necessarily return and may `goto` another block. And if the `return` statement is not
198+ included in the slice in such cases, it is necessary to terminate `selective_eval!` when
199+ execution reaches such implicit return statements. `controller.implicit_returns` records
200+ the PCs of such return statements, and `selective_eval!` will return when reaching those statements.
201+
202+ - **CFG short-cut**: When the successors of a conditional branch are inactive, and it is
203+ safe to move the program counter from the conditional branch to the nearest common
204+ post-dominator of those successors, this short-cut is taken.
205+ This short-cut is not merely an optimization but is actually essential for the correctness
206+ of the selective execution. This is because, in `CodeInfo`, even if we simply fall-through
207+ dead blocks (i.e., increment the program counter without executing the statements of those
208+ blocks), it does not necessarily lead to the nearest common post-dominator block.
209+
210+ These adjustments are necessary for performing selective execution correctly.
211+ [`lines_required`](@ref) or [`lines_required!`](@ref) will update the `SelectiveEvalController`
212+ passed as an argument to be appropriate for the program slice generated.
213+ """
214+ struct SelectiveEvalController{RC}
215+ inner:: RC # N.B. this doesn't support recursive selective evaluation
216+ implicit_returns:: BitSet # pc where selective execution should terminate even if they're inactive
217+ shortcuts:: Vector{CFGShortCut}
218+ SelectiveEvalController (inner:: RC = finish_and_return!) where RC = new {RC} (inner, BitSet (), CFGShortCut[])
219+ end
220+
185221function namedkeys (cl:: CodeLinks )
186222 ukeys = Set {GlobalRef} ()
187223 for c in (cl. namepreds, cl. namesuccs, cl. nameassigns)
@@ -591,8 +627,8 @@ function terminal_preds!(s, j, edges, covered) # can't be an inner function bec
591627end
592628
593629"""
594- isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges)
595- isrequired = lines_required(idx::Int, src::CodeInfo, edges::CodeEdges)
630+ isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController] )
631+ isrequired = lines_required(idx::Int, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController] )
596632
597633Determine which lines might need to be executed to evaluate `obj` or the statement indexed by `idx`.
598634If `isrequired[i]` is `false`, the `i`th statement is *not* required.
@@ -601,21 +637,26 @@ will end up skipping a subset of such statements, perhaps while repeating others
601637
602638See also [`lines_required!`](@ref) and [`selective_eval!`](@ref).
603639"""
604- function lines_required (obj:: GlobalRef , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
640+ function lines_required (obj:: GlobalRef , src:: CodeInfo , edges:: CodeEdges ,
641+ controller:: SelectiveEvalController = SelectiveEvalController ();
642+ kwargs... )
605643 isrequired = falses (length (edges. preds))
606644 objs = Set {GlobalRef} ([obj])
607- return lines_required! (isrequired, objs, src, edges; kwargs... )
645+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
608646end
609647
610- function lines_required (idx:: Int , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
648+ function lines_required (idx:: Int , src:: CodeInfo , edges:: CodeEdges ,
649+ controller:: SelectiveEvalController = SelectiveEvalController ();
650+ kwargs... )
611651 isrequired = falses (length (edges. preds))
612652 isrequired[idx] = true
613653 objs = Set {GlobalRef} ()
614- return lines_required! (isrequired, objs, src, edges; kwargs... )
654+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
615655end
616656
617657"""
618- lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges;
658+ lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges,
659+ [controller::SelectiveEvalController];
619660 norequire = ())
620661
621662Like `lines_required`, but where `isrequired[idx]` has already been set to `true` for all statements
@@ -627,9 +668,11 @@ should _not_ be marked as a requirement.
627668For example, use `norequire = LoweredCodeUtils.exclude_named_typedefs(src, edges)` if you're
628669extracting method signatures and not evaluating new definitions.
629670"""
630- function lines_required! (isrequired:: AbstractVector{Bool} , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
671+ function lines_required! (isrequired:: AbstractVector{Bool} , src:: CodeInfo , edges:: CodeEdges ,
672+ controller:: SelectiveEvalController = SelectiveEvalController ();
673+ kwargs... )
631674 objs = Set {GlobalRef} ()
632- return lines_required! (isrequired, objs, src, edges; kwargs... )
675+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
633676end
634677
635678function exclude_named_typedefs (src:: CodeInfo , edges:: CodeEdges )
@@ -649,7 +692,9 @@ function exclude_named_typedefs(src::CodeInfo, edges::CodeEdges)
649692 return norequire
650693end
651694
652- function lines_required! (isrequired:: AbstractVector{Bool} , objs, src:: CodeInfo , edges:: CodeEdges ; norequire = ())
695+ function lines_required! (isrequired:: AbstractVector{Bool} , objs, src:: CodeInfo , edges:: CodeEdges ,
696+ controller:: SelectiveEvalController = SelectiveEvalController ();
697+ norequire = ())
653698 # Mark any requested objects (their lines of assignment)
654699 objs = add_requests! (isrequired, objs, edges, norequire)
655700
@@ -684,7 +729,10 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
684729 end
685730
686731 # now mark the active goto nodes
687- add_active_gotos! (isrequired, src, cfg, postdomtree)
732+ add_active_gotos! (isrequired, src, cfg, postdomtree, controller)
733+
734+ # check if there are any implicit return blocks
735+ record_implcit_return! (controller, isrequired, cfg)
688736
689737 return isrequired
690738end
@@ -768,13 +816,14 @@ end
768816# The basic algorithm is based on what was proposed in [^Wei84]. If there is even one active
769817# block in the blocks reachable from a conditional branch up to its successors' nearest
770818# common post-dominator (referred to as 𝑰𝑵𝑭𝑳 in the paper), it is necessary to follow
771- # that conditional branch and execute the code. Otherwise, execution can be short-circuited
819+ # that conditional branch and execute the code. Otherwise, execution can be short-cut
772820# from the conditional branch to the nearest common post-dominator.
773821#
774- # COMBAK: It is important to note that in Julia's intermediate code representation (`CodeInfo`),
775- # "short-circuiting" a specific code region is not a simple task. Simply ignoring the path
776- # to the post-dominator does not guarantee fall-through to the post-dominator. Therefore,
777- # a more careful implementation is required for this aspect.
822+ # It is important to note that in Julia's intermediate code representation (`CodeInfo`),
823+ # "short-cutting" a specific code region is not a simple task. Simply incrementing the
824+ # program counter without executing the statements of 𝑰𝑵𝑭𝑳 blocks does not guarantee that
825+ # the program counter fall-throughs to the post-dominator.
826+ # To handle such cases, `selective_eval!` needs to use `SelectiveEvalController`.
778827#
779828# [Wei84]: M. Weiser, "Program Slicing," IEEE Transactions on Software Engineering, 10, pages 352-357, July 1984.
780829function add_control_flow! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
@@ -849,8 +898,8 @@ function reachable_blocks(cfg, from_bb::Int, to_bb::Int)
849898 return visited
850899end
851900
852- function add_active_gotos! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
853- dead_blocks = compute_dead_blocks (isrequired, src, cfg, postdomtree)
901+ function add_active_gotos! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree, controller :: SelectiveEvalController )
902+ dead_blocks = compute_dead_blocks! (isrequired, src, cfg, postdomtree, controller )
854903 changed = false
855904 for bbidx = 1 : length (cfg. blocks)
856905 if bbidx ∉ dead_blocks
@@ -868,7 +917,7 @@ function add_active_gotos!(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
868917end
869918
870919# find dead blocks using the same approach as `add_control_flow!`, for the converged `isrequired`
871- function compute_dead_blocks (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
920+ function compute_dead_blocks! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree, controller :: SelectiveEvalController )
872921 dead_blocks = BitSet ()
873922 for bbidx = 1 : length (cfg. blocks)
874923 bb = cfg. blocks[bbidx]
@@ -889,13 +938,31 @@ function compute_dead_blocks(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
889938 end
890939 if ! is_𝑰𝑵𝑭𝑳_active
891940 union! (dead_blocks, delete! (𝑰𝑵𝑭𝑳, postdominator))
941+ if postdominator ≠ 0
942+ postdominator_bb = cfg. blocks[postdominator]
943+ postdominator_entryidx = postdominator_bb. stmts[begin ]
944+ push! (controller. shortcuts, CFGShortCut (termidx, postdominator_entryidx))
945+ end
892946 end
893947 end
894948 end
895949 end
896950 return dead_blocks
897951end
898952
953+ function record_implcit_return! (controller:: SelectiveEvalController , isrequired, cfg:: CFG )
954+ for bbidx = 1 : length (cfg. blocks)
955+ bb = cfg. blocks[bbidx]
956+ if isempty (bb. succs)
957+ i = findfirst (idx:: Int -> ! isrequired[idx], bb. stmts)
958+ if ! isnothing (i)
959+ push! (controller. implicit_returns, bb. stmts[i])
960+ end
961+ end
962+ end
963+ nothing
964+ end
965+
899966# Do a traveral of "numbered" predecessors and find statement ranges and names of type definitions
900967function find_typedefs (src:: CodeInfo )
901968 typedef_blocks, typedef_names = UnitRange{Int}[], Symbol[]
@@ -1018,6 +1085,42 @@ function add_inplace!(isrequired, src, edges, norequire)
10181085 return changed
10191086end
10201087
1088+ function JuliaInterpreter. step_expr! (controller:: SelectiveEvalController , frame:: Frame , @nospecialize (node), istoplevel:: Bool )
1089+ if frame. pc in controller. implicit_returns
1090+ return nothing
1091+ elseif node isa GotoIfNot
1092+ for shortcut in controller. shortcuts
1093+ if shortcut. from == frame. pc
1094+ return frame. pc = shortcut. to
1095+ end
1096+ end
1097+ end
1098+ # TODO allow recursion: @invoke JuliaInterpreter.step_expr!(controller::Any, frame::Frame, node::Any, istoplevel::Bool)
1099+ JuliaInterpreter. step_expr! (controller. inner, frame, node, istoplevel)
1100+ end
1101+
1102+ next_or_nothing! (frame:: Frame ) = next_or_nothing! (finish_and_return!, frame)
1103+ function next_or_nothing! (@nospecialize (recurse), frame:: Frame )
1104+ pc = frame. pc
1105+ if pc < nstatements (frame. framecode)
1106+ return frame. pc = pc + 1
1107+ end
1108+ return nothing
1109+ end
1110+ function next_or_nothing! (controller:: SelectiveEvalController , frame:: Frame )
1111+ if frame. pc in controller. implicit_returns
1112+ return nothing
1113+ elseif pc_expr (frame) isa GotoIfNot
1114+ for shortcut in controller. shortcuts
1115+ if shortcut. from == frame. pc
1116+ return frame. pc = shortcut. to
1117+ end
1118+ end
1119+ end
1120+ # TODO allow recursion: @invoke next_or_nothing!(controller::Any, frame::Frame)
1121+ next_or_nothing! (controller. inner, frame)
1122+ end
1123+
10211124"""
10221125 struct SelectiveInterpreter{S<:Interpreter,T<:AbstractVector{Bool}} <: Interpreter
10231126 inner::S
@@ -1068,6 +1171,15 @@ See [`selective_eval_fromstart!`](@ref) to have that performed automatically.
10681171
10691172`isrequired` pertains only to `frame` itself, not any of its callees.
10701173
1174+ When `recurse::SelectiveEvalController` is specified, the selective evaluation execution
1175+ becomes fully correct. Conversely, with the default `finish_and_return!`, selective
1176+ evaluation may not be necessarily correct for all possible Julia code (see
1177+ https://github.com/JuliaDebug/LoweredCodeUtils.jl/pull/99 for more details).
1178+
1179+ Ensure that the specified `controller` is properly synchronized with `isrequired`.
1180+ Additionally note that, at present, it is not possible to recurse the `controller`.
1181+ In other words, there is no system in place for interprocedural selective evaluation.
1182+
10711183This will return either a `BreakpointRef`, the value obtained from the last executed statement
10721184(if stored to `frame.framedata.ssavlues`), or `nothing`.
10731185Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.
0 commit comments