@@ -182,6 +182,60 @@ 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. In such cases the actual `return` is an
198+ explicit statement earlier in the code, and if the slice does not include it,
199+ `selective_eval!` must still terminate when execution reaches it. `controller.implicit_returns`
200+ records the PCs of such `return` statements, and `selective_eval!` will return when reaching them.
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
215+ implicit_returns:: BitSet # pc where selective execution should terminate even if they're inactive
216+ shortcuts:: Vector{CFGShortCut}
217+ end
218+ SelectiveEvalController () = SelectiveEvalController (BitSet (), CFGShortCut[])
219+
220+ """
221+ struct SelectiveInterpreter{S<:Interpreter,T<:AbstractVector{Bool}} <: Interpreter
222+ inner::S
223+ isrequired::T
224+ controller::SelectiveEvalController
225+ end
226+
227+ An `JuliaInterpreter.Interpreter` that executes only the statements marked `true` in `isrequired`.
228+ Note that this interpreter does not recurse into callee frames.
229+ That is, when `JuliaInterpreter.finish!(interp::SelectiveInterpreter, frame, ...)` is
230+ performed, the `frame` will be executed selectively according to `interp.isrequired`, but
231+ any callee frames within it will be executed by `interp.inner::Interpreter`, not by `interp`.
232+ """
233+ struct SelectiveInterpreter{S<: Interpreter ,T<: AbstractVector{Bool} } <: Interpreter
234+ inner:: S
235+ isrequired:: T
236+ controller:: SelectiveEvalController
237+ end
238+
185239function namedkeys (cl:: CodeLinks )
186240 ukeys = Set {GlobalRef} ()
187241 for c in (cl. namepreds, cl. namesuccs, cl. nameassigns)
@@ -606,8 +660,8 @@ function terminal_preds!(s, j, edges, covered) # can't be an inner function bec
606660end
607661
608662"""
609- isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges)
610- isrequired = lines_required(idx::Int, src::CodeInfo, edges::CodeEdges)
663+ isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController] )
664+ isrequired = lines_required(idx::Int, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController] )
611665
612666Determine which lines might need to be executed to evaluate `obj` or the statement indexed by `idx`.
613667If `isrequired[i]` is `false`, the `i`th statement is *not* required.
@@ -616,21 +670,26 @@ will end up skipping a subset of such statements, perhaps while repeating others
616670
617671See also [`lines_required!`](@ref) and [`selective_eval!`](@ref).
618672"""
619- function lines_required (obj:: GlobalRef , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
673+ function lines_required (obj:: GlobalRef , src:: CodeInfo , edges:: CodeEdges ,
674+ controller:: SelectiveEvalController = SelectiveEvalController ();
675+ kwargs... )
620676 isrequired = falses (length (edges. preds))
621677 objs = Set {GlobalRef} ([obj])
622- return lines_required! (isrequired, objs, src, edges; kwargs... )
678+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
623679end
624680
625- function lines_required (idx:: Int , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
681+ function lines_required (idx:: Int , src:: CodeInfo , edges:: CodeEdges ,
682+ controller:: SelectiveEvalController = SelectiveEvalController ();
683+ kwargs... )
626684 isrequired = falses (length (edges. preds))
627685 isrequired[idx] = true
628686 objs = Set {GlobalRef} ()
629- return lines_required! (isrequired, objs, src, edges; kwargs... )
687+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
630688end
631689
632690"""
633- lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges;
691+ lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges,
692+ [controller::SelectiveEvalController];
634693 norequire = ())
635694
636695Like `lines_required`, but where `isrequired[idx]` has already been set to `true` for all statements
@@ -642,9 +701,11 @@ should _not_ be marked as a requirement.
642701For example, use `norequire = LoweredCodeUtils.exclude_named_typedefs(src, edges)` if you're
643702extracting method signatures and not evaluating new definitions.
644703"""
645- function lines_required! (isrequired:: AbstractVector{Bool} , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
704+ function lines_required! (isrequired:: AbstractVector{Bool} , src:: CodeInfo , edges:: CodeEdges ,
705+ controller:: SelectiveEvalController = SelectiveEvalController ();
706+ kwargs... )
646707 objs = Set {GlobalRef} ()
647- return lines_required! (isrequired, objs, src, edges; kwargs... )
708+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
648709end
649710
650711function exclude_named_typedefs (src:: CodeInfo , edges:: CodeEdges )
@@ -664,7 +725,9 @@ function exclude_named_typedefs(src::CodeInfo, edges::CodeEdges)
664725 return norequire
665726end
666727
667- function lines_required! (isrequired:: AbstractVector{Bool} , objs, src:: CodeInfo , edges:: CodeEdges ; norequire = ())
728+ function lines_required! (isrequired:: AbstractVector{Bool} , objs, src:: CodeInfo , edges:: CodeEdges ,
729+ controller:: SelectiveEvalController = SelectiveEvalController ();
730+ norequire = ())
668731 # Mark any requested objects (their lines of assignment)
669732 objs = add_requests! (isrequired, objs, edges, norequire)
670733
@@ -699,7 +762,10 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
699762 end
700763
701764 # now mark the active goto nodes
702- add_active_gotos! (isrequired, src, cfg, postdomtree)
765+ add_active_gotos! (isrequired, src, cfg, postdomtree, controller)
766+
767+ # check if there are any implicit return blocks
768+ record_implicit_return! (controller, isrequired, cfg)
703769
704770 return isrequired
705771end
@@ -777,19 +843,19 @@ end
777843
778844# # Add control-flow
779845
780-
781846# The goal of this function is to request concretization of the minimal necessary control
782847# flow to evaluate statements whose concretization have already been requested.
783848# The basic algorithm is based on what was proposed in [^Wei84]. If there is even one active
784849# block in the blocks reachable from a conditional branch up to its successors' nearest
785850# common post-dominator (referred to as 𝑰𝑵𝑭𝑳 in the paper), it is necessary to follow
786- # that conditional branch and execute the code. Otherwise, execution can be short-circuited
851+ # that conditional branch and execute the code. Otherwise, execution can be short-cut
787852# from the conditional branch to the nearest common post-dominator.
788853#
789- # COMBAK: It is important to note that in Julia's intermediate code representation (`CodeInfo`),
790- # "short-circuiting" a specific code region is not a simple task. Simply ignoring the path
791- # to the post-dominator does not guarantee fall-through to the post-dominator. Therefore,
792- # a more careful implementation is required for this aspect.
854+ # It is important to note that in Julia's intermediate code representation (`CodeInfo`),
855+ # "short-cutting" a specific code region is not a simple task. Simply incrementing the
856+ # program counter without executing the statements of 𝑰𝑵𝑭𝑳 blocks does not guarantee that
857+ # the program counter fall-throughs to the post-dominator.
858+ # To handle such cases, `selective_eval!` needs to use `SelectiveInterpreter`.
793859#
794860# [Wei84]: M. Weiser, "Program Slicing," IEEE Transactions on Software Engineering, 10, pages 352-357, July 1984.
795861function add_control_flow! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
@@ -864,8 +930,8 @@ function reachable_blocks(cfg, from_bb::Int, to_bb::Int)
864930 return visited
865931end
866932
867- function add_active_gotos! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
868- dead_blocks = compute_dead_blocks (isrequired, src, cfg, postdomtree)
933+ function add_active_gotos! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree, controller :: SelectiveEvalController )
934+ dead_blocks = compute_dead_blocks! (isrequired, src, cfg, postdomtree, controller )
869935 changed = false
870936 for bbidx = 1 : length (cfg. blocks)
871937 if bbidx ∉ dead_blocks
@@ -883,7 +949,7 @@ function add_active_gotos!(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
883949end
884950
885951# find dead blocks using the same approach as `add_control_flow!`, for the converged `isrequired`
886- function compute_dead_blocks (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
952+ function compute_dead_blocks! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree, controller :: SelectiveEvalController )
887953 dead_blocks = BitSet ()
888954 for bbidx = 1 : length (cfg. blocks)
889955 bb = cfg. blocks[bbidx]
@@ -904,13 +970,31 @@ function compute_dead_blocks(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
904970 end
905971 if ! is_𝑰𝑵𝑭𝑳_active
906972 union! (dead_blocks, delete! (𝑰𝑵𝑭𝑳, postdominator))
973+ if postdominator ≠ 0
974+ postdominator_bb = cfg. blocks[postdominator]
975+ postdominator_entryidx = postdominator_bb. stmts[begin ]
976+ push! (controller. shortcuts, CFGShortCut (termidx, postdominator_entryidx))
977+ end
907978 end
908979 end
909980 end
910981 end
911982 return dead_blocks
912983end
913984
985+ function record_implicit_return! (controller:: SelectiveEvalController , isrequired, cfg:: CFG )
986+ for bbidx = 1 : length (cfg. blocks)
987+ bb = cfg. blocks[bbidx]
988+ if isempty (bb. succs)
989+ i = findfirst (idx:: Int -> ! isrequired[idx], bb. stmts)
990+ if ! isnothing (i)
991+ push! (controller. implicit_returns, bb. stmts[i])
992+ end
993+ end
994+ end
995+ nothing
996+ end
997+
914998# Do a traveral of "numbered" predecessors and find statement ranges and names of type definitions
915999function find_typedefs (src:: CodeInfo )
9161000 typedef_blocks, typedef_names = UnitRange{Int}[], Symbol[]
@@ -1033,26 +1117,19 @@ function add_inplace!(isrequired, src, edges, norequire)
10331117 return changed
10341118end
10351119
1036- """
1037- struct SelectiveInterpreter{S<:Interpreter,T<:AbstractVector{Bool}} <: Interpreter
1038- inner::S
1039- isrequired::T
1040- end
1041-
1042- An `JuliaInterpreter.Interpreter` that executes only the statements marked `true` in `isrequired`.
1043- Note that this interpreter does not recurse into callee frames.
1044- That is, when `JuliaInterpreter.finish!(interp::SelectiveInterpreter, frame, ...)` is
1045- performed, the `frame` will be executed selectively according to `interp.isrequired`, but
1046- any callee frames within it will be executed by `interp.inner::Interpreter`, not by `interp`.
1047- """
1048- struct SelectiveInterpreter{S<: Interpreter ,T<: AbstractVector{Bool} } <: Interpreter
1049- inner:: S
1050- isrequired:: T
1051- end
10521120function JuliaInterpreter. step_expr! (interp:: SelectiveInterpreter , frame:: Frame , istoplevel:: Bool )
10531121 pc = frame. pc
1122+ if pc in interp. controller. implicit_returns
1123+ return nothing
1124+ elseif pc_expr (frame) isa GotoIfNot
1125+ for shortcut in interp. controller. shortcuts
1126+ if shortcut. from == pc
1127+ return frame. pc = shortcut. to
1128+ end
1129+ end
1130+ end
10541131 if interp. isrequired[pc]
1055- step_expr! (interp. inner, frame:: Frame , istoplevel:: Bool )
1132+ step_expr! (interp. inner, frame, istoplevel)
10561133 else
10571134 next_or_nothing! (interp, frame)
10581135 end
@@ -1083,12 +1160,28 @@ See [`selective_eval_fromstart!`](@ref) to have that performed automatically.
10831160
10841161`isrequired` pertains only to `frame` itself, not any of its callees.
10851162
1163+ By default `selective_eval!` runs with an empty [`SelectiveEvalController`](@ref), which
1164+ reproduces the historical behavior. That is correct for most top-level code, but not for
1165+ every possible Julia program: when the slice omits a `return` reached by fall-through, or
1166+ omits a branch whose dead region does not simply fall through to its post-dominator, the
1167+ interpreter can execute the wrong statements (see
1168+ https://github.com/JuliaDebug/LoweredCodeUtils.jl/pull/99 for details). For full correctness,
1169+ pass a [`SelectiveEvalController`](@ref) to [`lines_required`](@ref)/[`lines_required!`](@ref)
1170+ and reuse that same controller, together with the `isrequired` it produced, to construct a
1171+ [`SelectiveInterpreter`](@ref) that you run with `JuliaInterpreter.finish_and_return!`.
1172+ Drawing both from the same call is what keeps `isrequired` and the controller synchronized.
1173+
1174+ Note that the interpreter does not recurse into callees, so there is currently no
1175+ interprocedural selective evaluation.
1176+
10861177This will return either a `BreakpointRef`, the value obtained from the last executed statement
10871178(if stored to `frame.framedata.ssavlues`), or `nothing`.
10881179Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.
10891180"""
1090- selective_eval! (interp:: Interpreter , frame:: Frame , isrequired:: AbstractVector{Bool} , istoplevel:: Bool = false ) =
1091- JuliaInterpreter. finish_and_return! (SelectiveInterpreter (interp, isrequired), frame, istoplevel)
1181+ function selective_eval! (interp:: Interpreter , frame:: Frame , isrequired:: AbstractVector{Bool} , istoplevel:: Bool = false )
1182+ interp = SelectiveInterpreter (interp, isrequired, SelectiveEvalController ())
1183+ JuliaInterpreter. finish_and_return! (interp, frame, istoplevel)
1184+ end
10921185selective_eval! (args... ) = selective_eval! (RecursiveInterpreter (), args... )
10931186
10941187"""
0 commit comments