@@ -183,6 +183,42 @@ function postprint_linelinks(io::IO, idx::Int, src::CodeInfo, cl::CodeLinks, bbc
183183 return nothing
184184end
185185
186+ struct CFGShortCut
187+ from:: Int # pc of GotoIfNot with inactive 𝑰𝑵𝑭𝑳 blocks
188+ to:: Int # pc of the entry of the nearest common post-dominator of the GotoIfNot's successors
189+ end
190+
191+ """
192+ controller::SelectiveEvalController
193+
194+ When this object is passed as the `recurse` argument of `selective_eval!`,
195+ the selective execution is adjusted as follows:
196+
197+ - **Implicit return**: In Julia's IR representation (`CodeInfo`), the final block does not
198+ necessarily return and may `goto` another block. And if the `return` statement is not
199+ included in the slice in such cases, it is necessary to terminate `selective_eval!` when
200+ execution reaches such implicit return statements. `controller.implicit_returns` records
201+ the PCs of such return statements, and `selective_eval!` will return when reaching those statements.
202+
203+ - **CFG short-cut**: When the successors of a conditional branch are inactive, and it is
204+ safe to move the program counter from the conditional branch to the nearest common
205+ post-dominator of those successors, this short-cut is taken.
206+ This short-cut is not merely an optimization but is actually essential for the correctness
207+ of the selective execution. This is because, in `CodeInfo`, even if we simply fall-through
208+ dead blocks (i.e., increment the program counter without executing the statements of those
209+ blocks), it does not necessarily lead to the nearest common post-dominator block.
210+
211+ These adjustments are necessary for performing selective execution correctly.
212+ [`lines_required`](@ref) or [`lines_required!`](@ref) will update the `SelectiveEvalController`
213+ passed as an argument to be appropriate for the program slice generated.
214+ """
215+ struct SelectiveEvalController{RC}
216+ inner:: RC # N.B. this doesn't support recursive selective evaluation
217+ implicit_returns:: BitSet # pc where selective execution should terminate even if they're inactive
218+ shortcuts:: Vector{CFGShortCut}
219+ SelectiveEvalController (inner:: RC = finish_and_return!) where RC = new {RC} (inner, BitSet (), CFGShortCut[])
220+ end
221+
186222function namedkeys (cl:: CodeLinks )
187223 ukeys = Set {GlobalRef} ()
188224 for c in (cl. namepreds, cl. namesuccs, cl. nameassigns)
@@ -573,8 +609,8 @@ function terminal_preds!(s, j, edges, covered) # can't be an inner function bec
573609end
574610
575611"""
576- isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges)
577- isrequired = lines_required(idx::Int, src::CodeInfo, edges::CodeEdges)
612+ isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController] )
613+ isrequired = lines_required(idx::Int, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController] )
578614
579615Determine which lines might need to be executed to evaluate `obj` or the statement indexed by `idx`.
580616If `isrequired[i]` is `false`, the `i`th statement is *not* required.
@@ -583,21 +619,26 @@ will end up skipping a subset of such statements, perhaps while repeating others
583619
584620See also [`lines_required!`](@ref) and [`selective_eval!`](@ref).
585621"""
586- function lines_required (obj:: GlobalRef , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
622+ function lines_required (obj:: GlobalRef , src:: CodeInfo , edges:: CodeEdges ,
623+ controller:: SelectiveEvalController = SelectiveEvalController ();
624+ kwargs... )
587625 isrequired = falses (length (edges. preds))
588626 objs = Set {GlobalRef} ([obj])
589- return lines_required! (isrequired, objs, src, edges; kwargs... )
627+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
590628end
591629
592- function lines_required (idx:: Int , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
630+ function lines_required (idx:: Int , src:: CodeInfo , edges:: CodeEdges ,
631+ controller:: SelectiveEvalController = SelectiveEvalController ();
632+ kwargs... )
593633 isrequired = falses (length (edges. preds))
594634 isrequired[idx] = true
595635 objs = Set {GlobalRef} ()
596- return lines_required! (isrequired, objs, src, edges; kwargs... )
636+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
597637end
598638
599639"""
600- lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges;
640+ lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges,
641+ [controller::SelectiveEvalController];
601642 norequire = ())
602643
603644Like `lines_required`, but where `isrequired[idx]` has already been set to `true` for all statements
@@ -609,9 +650,11 @@ should _not_ be marked as a requirement.
609650For example, use `norequire = LoweredCodeUtils.exclude_named_typedefs(src, edges)` if you're
610651extracting method signatures and not evaluating new definitions.
611652"""
612- function lines_required! (isrequired:: AbstractVector{Bool} , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
653+ function lines_required! (isrequired:: AbstractVector{Bool} , src:: CodeInfo , edges:: CodeEdges ,
654+ controller:: SelectiveEvalController = SelectiveEvalController ();
655+ kwargs... )
613656 objs = Set {GlobalRef} ()
614- return lines_required! (isrequired, objs, src, edges; kwargs... )
657+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
615658end
616659
617660function exclude_named_typedefs (src:: CodeInfo , edges:: CodeEdges )
@@ -631,7 +674,9 @@ function exclude_named_typedefs(src::CodeInfo, edges::CodeEdges)
631674 return norequire
632675end
633676
634- function lines_required! (isrequired:: AbstractVector{Bool} , objs, src:: CodeInfo , edges:: CodeEdges ; norequire = ())
677+ function lines_required! (isrequired:: AbstractVector{Bool} , objs, src:: CodeInfo , edges:: CodeEdges ,
678+ controller:: SelectiveEvalController = SelectiveEvalController ();
679+ norequire = ())
635680 # Mark any requested objects (their lines of assignment)
636681 objs = add_requests! (isrequired, objs, edges, norequire)
637682
@@ -666,7 +711,10 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
666711 end
667712
668713 # now mark the active goto nodes
669- add_active_gotos! (isrequired, src, cfg, postdomtree)
714+ add_active_gotos! (isrequired, src, cfg, postdomtree, controller)
715+
716+ # check if there are any implicit return blocks
717+ record_implcit_return! (controller, isrequired, cfg)
670718
671719 return isrequired
672720end
@@ -745,13 +793,14 @@ using Core.Compiler: CFG, BasicBlock, compute_basic_blocks
745793# The basic algorithm is based on what was proposed in [^Wei84]. If there is even one active
746794# block in the blocks reachable from a conditional branch up to its successors' nearest
747795# common post-dominator (referred to as 𝑰𝑵𝑭𝑳 in the paper), it is necessary to follow
748- # that conditional branch and execute the code. Otherwise, execution can be short-circuited
796+ # that conditional branch and execute the code. Otherwise, execution can be short-cut
749797# from the conditional branch to the nearest common post-dominator.
750798#
751- # COMBAK: It is important to note that in Julia's intermediate code representation (`CodeInfo`),
752- # "short-circuiting" a specific code region is not a simple task. Simply ignoring the path
753- # to the post-dominator does not guarantee fall-through to the post-dominator. Therefore,
754- # a more careful implementation is required for this aspect.
799+ # It is important to note that in Julia's intermediate code representation (`CodeInfo`),
800+ # "short-cutting" a specific code region is not a simple task. Simply incrementing the
801+ # program counter without executing the statements of 𝑰𝑵𝑭𝑳 blocks does not guarantee that
802+ # the program counter fall-throughs to the post-dominator.
803+ # To handle such cases, `selective_eval!` needs to use `SelectiveEvalController`.
755804#
756805# [Wei84]: M. Weiser, "Program Slicing," IEEE Transactions on Software Engineering, 10, pages 352-357, July 1984.
757806function add_control_flow! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
@@ -826,8 +875,8 @@ function reachable_blocks(cfg, from_bb::Int, to_bb::Int)
826875 return visited
827876end
828877
829- function add_active_gotos! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
830- dead_blocks = compute_dead_blocks (isrequired, src, cfg, postdomtree)
878+ function add_active_gotos! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree, controller :: SelectiveEvalController )
879+ dead_blocks = compute_dead_blocks! (isrequired, src, cfg, postdomtree, controller )
831880 changed = false
832881 for bbidx = 1 : length (cfg. blocks)
833882 if bbidx ∉ dead_blocks
@@ -845,7 +894,7 @@ function add_active_gotos!(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
845894end
846895
847896# find dead blocks using the same approach as `add_control_flow!`, for the converged `isrequired`
848- function compute_dead_blocks (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
897+ function compute_dead_blocks! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree, controller :: SelectiveEvalController )
849898 dead_blocks = BitSet ()
850899 for bbidx = 1 : length (cfg. blocks)
851900 bb = cfg. blocks[bbidx]
@@ -866,13 +915,31 @@ function compute_dead_blocks(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
866915 end
867916 if ! is_𝑰𝑵𝑭𝑳_active
868917 union! (dead_blocks, delete! (𝑰𝑵𝑭𝑳, postdominator))
918+ if postdominator ≠ 0
919+ postdominator_bb = cfg. blocks[postdominator]
920+ postdominator_entryidx = postdominator_bb. stmts[begin ]
921+ push! (controller. shortcuts, CFGShortCut (termidx, postdominator_entryidx))
922+ end
869923 end
870924 end
871925 end
872926 end
873927 return dead_blocks
874928end
875929
930+ function record_implcit_return! (controller:: SelectiveEvalController , isrequired, cfg:: CFG )
931+ for bbidx = 1 : length (cfg. blocks)
932+ bb = cfg. blocks[bbidx]
933+ if isempty (bb. succs)
934+ i = findfirst (idx:: Int -> ! isrequired[idx], bb. stmts)
935+ if ! isnothing (i)
936+ push! (controller. implicit_returns, bb. stmts[i])
937+ end
938+ end
939+ end
940+ nothing
941+ end
942+
876943# Do a traveral of "numbered" predecessors and find statement ranges and names of type definitions
877944function find_typedefs (src:: CodeInfo )
878945 typedef_blocks, typedef_names = UnitRange{Int}[], Symbol[]
@@ -995,6 +1062,42 @@ function add_inplace!(isrequired, src, edges, norequire)
9951062 return changed
9961063end
9971064
1065+ function JuliaInterpreter. step_expr! (controller:: SelectiveEvalController , frame:: Frame , @nospecialize (node), istoplevel:: Bool )
1066+ if frame. pc in controller. implicit_returns
1067+ return nothing
1068+ elseif node isa GotoIfNot
1069+ for shortcut in controller. shortcuts
1070+ if shortcut. from == frame. pc
1071+ return frame. pc = shortcut. to
1072+ end
1073+ end
1074+ end
1075+ # TODO allow recursion: @invoke JuliaInterpreter.step_expr!(controller::Any, frame::Frame, node::Any, istoplevel::Bool)
1076+ JuliaInterpreter. step_expr! (controller. inner, frame, node, istoplevel)
1077+ end
1078+
1079+ next_or_nothing! (frame:: Frame ) = next_or_nothing! (finish_and_return!, frame)
1080+ function next_or_nothing! (@nospecialize (recurse), frame:: Frame )
1081+ pc = frame. pc
1082+ if pc < nstatements (frame. framecode)
1083+ return frame. pc = pc + 1
1084+ end
1085+ return nothing
1086+ end
1087+ function next_or_nothing! (controller:: SelectiveEvalController , frame:: Frame )
1088+ if frame. pc in controller. implicit_returns
1089+ return nothing
1090+ elseif pc_expr (frame) isa GotoIfNot
1091+ for shortcut in controller. shortcuts
1092+ if shortcut. from == frame. pc
1093+ return frame. pc = shortcut. to
1094+ end
1095+ end
1096+ end
1097+ # TODO allow recursion: @invoke next_or_nothing!(controller::Any, frame::Frame)
1098+ next_or_nothing! (controller. inner, frame)
1099+ end
1100+
9981101"""
9991102 selective_eval!([recurse], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)
10001103
@@ -1007,6 +1110,15 @@ See [`selective_eval_fromstart!`](@ref) to have that performed automatically.
10071110The default value for `recurse` is `JuliaInterpreter.finish_and_return!`.
10081111`isrequired` pertains only to `frame` itself, not any of its callees.
10091112
1113+ When `recurse::SelectiveEvalController` is specified, the selective evaluation execution
1114+ becomes fully correct. Conversely, with the default `finish_and_return!`, selective
1115+ evaluation may not be necessarily correct for all possible Julia code (see
1116+ https://github.com/JuliaDebug/LoweredCodeUtils.jl/pull/99 for more details).
1117+
1118+ Ensure that the specified `controller` is properly synchronized with `isrequired`.
1119+ Additionally note that, at present, it is not possible to recurse the `controller`.
1120+ In other words, there is no system in place for interprocedural selective evaluation.
1121+
10101122This will return either a `BreakpointRef`, the value obtained from the last executed statement
10111123(if stored to `frame.framedata.ssavlues`), or `nothing`.
10121124Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.
0 commit comments