@@ -173,6 +173,42 @@ function postprint_linelinks(io::IO, idx::Int, src::CodeInfo, cl::CodeLinks, bbc
173173 return nothing
174174end
175175
176+ struct CFGShortCut
177+ from:: Int # pc of GotoIfNot with inactive 𝑰𝑵𝑭𝑳 blocks
178+ to:: Int # pc of the entry of the nearest common post-dominator of the GotoIfNot's successors
179+ end
180+
181+ """
182+ controller::SelectiveEvalController
183+
184+ When this object is passed as the `recurse` argument of `selective_eval!`,
185+ the selective execution is adjusted as follows:
186+
187+ - **Implicit return**: In Julia's IR representation (`CodeInfo`), the final block does not
188+ necessarily return and may `goto` another block. And if the `return` statement is not
189+ included in the slice in such cases, it is necessary to terminate `selective_eval!` when
190+ execution reaches such implicit return statements. `controller.implicit_returns` records
191+ the PCs of such return statements, and `selective_eval!` will return when reaching those statements.
192+
193+ - **CFG short-cut**: When the successors of a conditional branch are inactive, and it is
194+ safe to move the program counter from the conditional branch to the nearest common
195+ post-dominator of those successors, this short-cut is taken.
196+ This short-cut is not merely an optimization but is actually essential for the correctness
197+ of the selective execution. This is because, in `CodeInfo`, even if we simply fall-through
198+ dead blocks (i.e., increment the program counter without executing the statements of those
199+ blocks), it does not necessarily lead to the nearest common post-dominator block.
200+
201+ These adjustments are necessary for performing selective execution correctly.
202+ [`lines_required`](@ref) or [`lines_required!`](@ref) will update the `SelectiveEvalController`
203+ passed as an argument to be appropriate for the program slice generated.
204+ """
205+ struct SelectiveEvalController{RC}
206+ inner:: RC # N.B. this doesn't support recursive selective evaluation
207+ implicit_returns:: BitSet # pc where selective execution should terminate even if they're inactive
208+ shortcuts:: Vector{CFGShortCut}
209+ SelectiveEvalController (inner:: RC = finish_and_return!) where RC = new {RC} (inner, BitSet (), CFGShortCut[])
210+ end
211+
176212function namedkeys (cl:: CodeLinks )
177213 ukeys = Set {GlobalRef} ()
178214 for c in (cl. namepreds, cl. namesuccs, cl. nameassigns)
@@ -563,8 +599,8 @@ function terminal_preds!(s, j, edges, covered) # can't be an inner function bec
563599end
564600
565601"""
566- isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges)
567- isrequired = lines_required(idx::Int, src::CodeInfo, edges::CodeEdges)
602+ isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController] )
603+ isrequired = lines_required(idx::Int, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController] )
568604
569605Determine which lines might need to be executed to evaluate `obj` or the statement indexed by `idx`.
570606If `isrequired[i]` is `false`, the `i`th statement is *not* required.
@@ -573,21 +609,26 @@ will end up skipping a subset of such statements, perhaps while repeating others
573609
574610See also [`lines_required!`](@ref) and [`selective_eval!`](@ref).
575611"""
576- function lines_required (obj:: GlobalRef , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
612+ function lines_required (obj:: GlobalRef , src:: CodeInfo , edges:: CodeEdges ,
613+ controller:: SelectiveEvalController = SelectiveEvalController ();
614+ kwargs... )
577615 isrequired = falses (length (edges. preds))
578616 objs = Set {GlobalRef} ([obj])
579- return lines_required! (isrequired, objs, src, edges; kwargs... )
617+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
580618end
581619
582- function lines_required (idx:: Int , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
620+ function lines_required (idx:: Int , src:: CodeInfo , edges:: CodeEdges ,
621+ controller:: SelectiveEvalController = SelectiveEvalController ();
622+ kwargs... )
583623 isrequired = falses (length (edges. preds))
584624 isrequired[idx] = true
585625 objs = Set {GlobalRef} ()
586- return lines_required! (isrequired, objs, src, edges; kwargs... )
626+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
587627end
588628
589629"""
590- lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges;
630+ lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges,
631+ [controller::SelectiveEvalController];
591632 norequire = ())
592633
593634Like `lines_required`, but where `isrequired[idx]` has already been set to `true` for all statements
@@ -599,9 +640,11 @@ should _not_ be marked as a requirement.
599640For example, use `norequire = LoweredCodeUtils.exclude_named_typedefs(src, edges)` if you're
600641extracting method signatures and not evaluating new definitions.
601642"""
602- function lines_required! (isrequired:: AbstractVector{Bool} , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
643+ function lines_required! (isrequired:: AbstractVector{Bool} , src:: CodeInfo , edges:: CodeEdges ,
644+ controller:: SelectiveEvalController = SelectiveEvalController ();
645+ kwargs... )
603646 objs = Set {GlobalRef} ()
604- return lines_required! (isrequired, objs, src, edges; kwargs... )
647+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
605648end
606649
607650function exclude_named_typedefs (src:: CodeInfo , edges:: CodeEdges )
@@ -621,7 +664,9 @@ function exclude_named_typedefs(src::CodeInfo, edges::CodeEdges)
621664 return norequire
622665end
623666
624- function lines_required! (isrequired:: AbstractVector{Bool} , objs, src:: CodeInfo , edges:: CodeEdges ; norequire = ())
667+ function lines_required! (isrequired:: AbstractVector{Bool} , objs, src:: CodeInfo , edges:: CodeEdges ,
668+ controller:: SelectiveEvalController = SelectiveEvalController ();
669+ norequire = ())
625670 # Mark any requested objects (their lines of assignment)
626671 objs = add_requests! (isrequired, objs, edges, norequire)
627672
@@ -656,7 +701,10 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
656701 end
657702
658703 # now mark the active goto nodes
659- add_active_gotos! (isrequired, src, cfg, postdomtree)
704+ add_active_gotos! (isrequired, src, cfg, postdomtree, controller)
705+
706+ # check if there are any implicit return blocks
707+ record_implcit_return! (controller, isrequired, cfg)
660708
661709 return isrequired
662710end
@@ -735,13 +783,14 @@ using Core.Compiler: CFG, BasicBlock, compute_basic_blocks
735783# The basic algorithm is based on what was proposed in [^Wei84]. If there is even one active
736784# block in the blocks reachable from a conditional branch up to its successors' nearest
737785# common post-dominator (referred to as 𝑰𝑵𝑭𝑳 in the paper), it is necessary to follow
738- # that conditional branch and execute the code. Otherwise, execution can be short-circuited
786+ # that conditional branch and execute the code. Otherwise, execution can be short-cut
739787# from the conditional branch to the nearest common post-dominator.
740788#
741- # COMBAK: It is important to note that in Julia's intermediate code representation (`CodeInfo`),
742- # "short-circuiting" a specific code region is not a simple task. Simply ignoring the path
743- # to the post-dominator does not guarantee fall-through to the post-dominator. Therefore,
744- # a more careful implementation is required for this aspect.
789+ # It is important to note that in Julia's intermediate code representation (`CodeInfo`),
790+ # "short-cutting" a specific code region is not a simple task. Simply incrementing the
791+ # program counter without executing the statements of 𝑰𝑵𝑭𝑳 blocks does not guarantee that
792+ # the program counter fall-throughs to the post-dominator.
793+ # To handle such cases, `selective_eval!` needs to use `SelectiveEvalController`.
745794#
746795# [Wei84]: M. Weiser, "Program Slicing," IEEE Transactions on Software Engineering, 10, pages 352-357, July 1984.
747796function add_control_flow! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
@@ -816,8 +865,8 @@ function reachable_blocks(cfg, from_bb::Int, to_bb::Int)
816865 return visited
817866end
818867
819- function add_active_gotos! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
820- dead_blocks = compute_dead_blocks (isrequired, src, cfg, postdomtree)
868+ function add_active_gotos! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree, controller :: SelectiveEvalController )
869+ dead_blocks = compute_dead_blocks! (isrequired, src, cfg, postdomtree, controller )
821870 changed = false
822871 for bbidx = 1 : length (cfg. blocks)
823872 if bbidx ∉ dead_blocks
@@ -835,7 +884,7 @@ function add_active_gotos!(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
835884end
836885
837886# find dead blocks using the same approach as `add_control_flow!`, for the converged `isrequired`
838- function compute_dead_blocks (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
887+ function compute_dead_blocks! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree, controller :: SelectiveEvalController )
839888 dead_blocks = BitSet ()
840889 for bbidx = 1 : length (cfg. blocks)
841890 bb = cfg. blocks[bbidx]
@@ -856,13 +905,31 @@ function compute_dead_blocks(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
856905 end
857906 if ! is_𝑰𝑵𝑭𝑳_active
858907 union! (dead_blocks, delete! (𝑰𝑵𝑭𝑳, postdominator))
908+ if postdominator ≠ 0
909+ postdominator_bb = cfg. blocks[postdominator]
910+ postdominator_entryidx = postdominator_bb. stmts[begin ]
911+ push! (controller. shortcuts, CFGShortCut (termidx, postdominator_entryidx))
912+ end
859913 end
860914 end
861915 end
862916 end
863917 return dead_blocks
864918end
865919
920+ function record_implcit_return! (controller:: SelectiveEvalController , isrequired, cfg:: CFG )
921+ for bbidx = 1 : length (cfg. blocks)
922+ bb = cfg. blocks[bbidx]
923+ if isempty (bb. succs)
924+ i = findfirst (idx:: Int -> ! isrequired[idx], bb. stmts)
925+ if ! isnothing (i)
926+ push! (controller. implicit_returns, bb. stmts[i])
927+ end
928+ end
929+ end
930+ nothing
931+ end
932+
866933# Do a traveral of "numbered" predecessors and find statement ranges and names of type definitions
867934function find_typedefs (src:: CodeInfo )
868935 typedef_blocks, typedef_names = UnitRange{Int}[], Symbol[]
@@ -985,6 +1052,42 @@ function add_inplace!(isrequired, src, edges, norequire)
9851052 return changed
9861053end
9871054
1055+ function JuliaInterpreter. step_expr! (controller:: SelectiveEvalController , frame:: Frame , @nospecialize (node), istoplevel:: Bool )
1056+ if frame. pc in controller. implicit_returns
1057+ return nothing
1058+ elseif node isa GotoIfNot
1059+ for shortcut in controller. shortcuts
1060+ if shortcut. from == frame. pc
1061+ return frame. pc = shortcut. to
1062+ end
1063+ end
1064+ end
1065+ # TODO allow recursion: @invoke JuliaInterpreter.step_expr!(controller::Any, frame::Frame, node::Any, istoplevel::Bool)
1066+ JuliaInterpreter. step_expr! (controller. inner, frame, node, istoplevel)
1067+ end
1068+
1069+ next_or_nothing! (frame:: Frame ) = next_or_nothing! (finish_and_return!, frame)
1070+ function next_or_nothing! (@nospecialize (recurse), frame:: Frame )
1071+ pc = frame. pc
1072+ if pc < nstatements (frame. framecode)
1073+ return frame. pc = pc + 1
1074+ end
1075+ return nothing
1076+ end
1077+ function next_or_nothing! (controller:: SelectiveEvalController , frame:: Frame )
1078+ if frame. pc in controller. implicit_returns
1079+ return nothing
1080+ elseif pc_expr (frame) isa GotoIfNot
1081+ for shortcut in controller. shortcuts
1082+ if shortcut. from == frame. pc
1083+ return frame. pc = shortcut. to
1084+ end
1085+ end
1086+ end
1087+ # TODO allow recursion: @invoke next_or_nothing!(controller::Any, frame::Frame)
1088+ next_or_nothing! (controller. inner, frame)
1089+ end
1090+
9881091"""
9891092 selective_eval!([recurse], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)
9901093
@@ -997,6 +1100,15 @@ See [`selective_eval_fromstart!`](@ref) to have that performed automatically.
9971100The default value for `recurse` is `JuliaInterpreter.finish_and_return!`.
9981101`isrequired` pertains only to `frame` itself, not any of its callees.
9991102
1103+ When `recurse::SelectiveEvalController` is specified, the selective evaluation execution
1104+ becomes fully correct. Conversely, with the default `finish_and_return!`, selective
1105+ evaluation may not be necessarily correct for all possible Julia code (see
1106+ https://github.com/JuliaDebug/LoweredCodeUtils.jl/pull/99 for more details).
1107+
1108+ Ensure that the specified `controller` is properly synchronized with `isrequired`.
1109+ Additionally note that, at present, it is not possible to recurse the `controller`.
1110+ In other words, there is no system in place for interprocedural selective evaluation.
1111+
10001112This will return either a `BreakpointRef`, the value obtained from the last executed statement
10011113(if stored to `frame.framedata.ssavlues`), or `nothing`.
10021114Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.
0 commit comments