@@ -173,6 +173,41 @@ 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
176211
177212function namedkeys (cl:: CodeLinks )
178213 ukeys = Set {GlobalRef} ()
566601
567602
568603"""
569- isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges)
570- isrequired = lines_required(idx::Int, src::CodeInfo, edges::CodeEdges)
604+ isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController] )
605+ isrequired = lines_required(idx::Int, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController] )
571606
572607Determine which lines might need to be executed to evaluate `obj` or the statement indexed by `idx`.
573608If `isrequired[i]` is `false`, the `i`th statement is *not* required.
@@ -576,21 +611,26 @@ will end up skipping a subset of such statements, perhaps while repeating others
576611
577612See also [`lines_required!`](@ref) and [`selective_eval!`](@ref).
578613"""
579- function lines_required (obj:: GlobalRef , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
614+ function lines_required (obj:: GlobalRef , src:: CodeInfo , edges:: CodeEdges ,
615+ controller:: SelectiveEvalController = SelectiveEvalController ();
616+ kwargs... )
580617 isrequired = falses (length (edges. preds))
581618 objs = Set {GlobalRef} ([obj])
582- return lines_required! (isrequired, objs, src, edges; kwargs... )
619+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
583620end
584621
585- function lines_required (idx:: Int , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
622+ function lines_required (idx:: Int , src:: CodeInfo , edges:: CodeEdges ,
623+ controller:: SelectiveEvalController = SelectiveEvalController ();
624+ kwargs... )
586625 isrequired = falses (length (edges. preds))
587626 isrequired[idx] = true
588627 objs = Set {GlobalRef} ()
589- return lines_required! (isrequired, objs, src, edges; kwargs... )
628+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
590629end
591630
592631"""
593- lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges;
632+ lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges,
633+ [controller::SelectiveEvalController];
594634 norequire = ())
595635
596636Like `lines_required`, but where `isrequired[idx]` has already been set to `true` for all statements
@@ -602,9 +642,11 @@ should _not_ be marked as a requirement.
602642For example, use `norequire = LoweredCodeUtils.exclude_named_typedefs(src, edges)` if you're
603643extracting method signatures and not evaluating new definitions.
604644"""
605- function lines_required! (isrequired:: AbstractVector{Bool} , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
645+ function lines_required! (isrequired:: AbstractVector{Bool} , src:: CodeInfo , edges:: CodeEdges ,
646+ controller:: SelectiveEvalController = SelectiveEvalController ();
647+ kwargs... )
606648 objs = Set {GlobalRef} ()
607- return lines_required! (isrequired, objs, src, edges; kwargs... )
649+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
608650end
609651
610652function exclude_named_typedefs (src:: CodeInfo , edges:: CodeEdges )
@@ -624,7 +666,9 @@ function exclude_named_typedefs(src::CodeInfo, edges::CodeEdges)
624666 return norequire
625667end
626668
627- function lines_required! (isrequired:: AbstractVector{Bool} , objs, src:: CodeInfo , edges:: CodeEdges ; norequire = ())
669+ function lines_required! (isrequired:: AbstractVector{Bool} , objs, src:: CodeInfo , edges:: CodeEdges ,
670+ controller:: SelectiveEvalController = SelectiveEvalController ();
671+ norequire = ())
628672 # Mark any requested objects (their lines of assignment)
629673 objs = add_requests! (isrequired, objs, edges, norequire)
630674
@@ -659,7 +703,10 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
659703 end
660704
661705 # now mark the active goto nodes
662- add_active_gotos! (isrequired, src, cfg, postdomtree)
706+ add_active_gotos! (isrequired, src, cfg, postdomtree, controller)
707+
708+ # check if there are any implicit return blocks
709+ record_implcit_return! (controller, isrequired, cfg)
663710
664711 return isrequired
665712end
@@ -738,13 +785,14 @@ using Core.Compiler: CFG, BasicBlock, compute_basic_blocks
738785# The basic algorithm is based on what was proposed in [^Wei84]. If there is even one active
739786# block in the blocks reachable from a conditional branch up to its successors' nearest
740787# common post-dominator (referred to as 𝑰𝑵𝑭𝑳 in the paper), it is necessary to follow
741- # that conditional branch and execute the code. Otherwise, execution can be short-circuited
788+ # that conditional branch and execute the code. Otherwise, execution can be short-cut
742789# from the conditional branch to the nearest common post-dominator.
743790#
744- # COMBAK: It is important to note that in Julia's intermediate code representation (`CodeInfo`),
745- # "short-circuiting" a specific code region is not a simple task. Simply ignoring the path
746- # to the post-dominator does not guarantee fall-through to the post-dominator. Therefore,
747- # a more careful implementation is required for this aspect.
791+ # It is important to note that in Julia's intermediate code representation (`CodeInfo`),
792+ # "short-cutting" a specific code region is not a simple task. Simply incrementing the
793+ # program counter without executing the statements of 𝑰𝑵𝑭𝑳 blocks does not guarantee that
794+ # the program counter fall-throughs to the post-dominator.
795+ # To handle such cases, `selective_eval!` needs to use `SelectiveEvalController`.
748796#
749797# [Wei84]: M. Weiser, "Program Slicing," IEEE Transactions on Software Engineering, 10, pages 352-357, July 1984.
750798function add_control_flow! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
@@ -819,8 +867,8 @@ function reachable_blocks(cfg, from_bb::Int, to_bb::Int)
819867 return visited
820868end
821869
822- function add_active_gotos! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
823- dead_blocks = compute_dead_blocks (isrequired, src, cfg, postdomtree)
870+ function add_active_gotos! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree, controller :: SelectiveEvalController )
871+ dead_blocks = compute_dead_blocks! (isrequired, src, cfg, postdomtree, controller )
824872 changed = false
825873 for bbidx = 1 : length (cfg. blocks)
826874 if bbidx ∉ dead_blocks
@@ -838,7 +886,7 @@ function add_active_gotos!(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
838886end
839887
840888# find dead blocks using the same approach as `add_control_flow!`, for the converged `isrequired`
841- function compute_dead_blocks (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
889+ function compute_dead_blocks! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree, controller :: SelectiveEvalController )
842890 dead_blocks = BitSet ()
843891 for bbidx = 1 : length (cfg. blocks)
844892 bb = cfg. blocks[bbidx]
@@ -859,13 +907,31 @@ function compute_dead_blocks(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
859907 end
860908 if ! is_𝑰𝑵𝑭𝑳_active
861909 union! (dead_blocks, delete! (𝑰𝑵𝑭𝑳, postdominator))
910+ if postdominator ≠ 0
911+ postdominator_bb = cfg. blocks[postdominator]
912+ postdominator_entryidx = postdominator_bb. stmts[begin ]
913+ push! (controller. shortcuts, CFGShortCut (termidx, postdominator_entryidx))
914+ end
862915 end
863916 end
864917 end
865918 end
866919 return dead_blocks
867920end
868921
922+ function record_implcit_return! (controller:: SelectiveEvalController , isrequired, cfg:: CFG )
923+ for bbidx = 1 : length (cfg. blocks)
924+ bb = cfg. blocks[bbidx]
925+ if isempty (bb. succs)
926+ i = findfirst (idx:: Int -> ! isrequired[idx], bb. stmts)
927+ if ! isnothing (i)
928+ push! (controller. implicit_returns, bb. stmts[i])
929+ end
930+ end
931+ end
932+ nothing
933+ end
934+
869935# Do a traveral of "numbered" predecessors and find statement ranges and names of type definitions
870936function find_typedefs (src:: CodeInfo )
871937 typedef_blocks, typedef_names = UnitRange{Int}[], Symbol[]
@@ -988,6 +1054,42 @@ function add_inplace!(isrequired, src, edges, norequire)
9881054 return changed
9891055end
9901056
1057+ function JuliaInterpreter. step_expr! (controller:: SelectiveEvalController , frame:: Frame , @nospecialize (node), istoplevel:: Bool )
1058+ if frame. pc in controller. implicit_returns
1059+ return nothing
1060+ elseif node isa GotoIfNot
1061+ for shortcut in controller. shortcuts
1062+ if shortcut. from == frame. pc
1063+ return frame. pc = shortcut. to
1064+ end
1065+ end
1066+ end
1067+ # TODO allow recursion: @invoke JuliaInterpreter.step_expr!(controller::Any, frame::Frame, node::Any, istoplevel::Bool)
1068+ JuliaInterpreter. step_expr! (controller. inner, frame, node, istoplevel)
1069+ end
1070+
1071+ next_or_nothing! (frame:: Frame ) = next_or_nothing! (finish_and_return!, frame)
1072+ function next_or_nothing! (@nospecialize (recurse), frame:: Frame )
1073+ pc = frame. pc
1074+ if pc < nstatements (frame. framecode)
1075+ return frame. pc = pc + 1
1076+ end
1077+ return nothing
1078+ end
1079+ function next_or_nothing! (controller:: SelectiveEvalController , frame:: Frame )
1080+ if frame. pc in controller. implicit_returns
1081+ return nothing
1082+ elseif pc_expr (frame) isa GotoIfNot
1083+ for shortcut in controller. shortcuts
1084+ if shortcut. from == frame. pc
1085+ return frame. pc = shortcut. to
1086+ end
1087+ end
1088+ end
1089+ # TODO allow recursion: @invoke next_or_nothing!(controller::Any, frame::Frame)
1090+ next_or_nothing! (controller. inner, frame)
1091+ end
1092+
9911093"""
9921094 selective_eval!([recurse], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)
9931095
@@ -1000,6 +1102,15 @@ See [`selective_eval_fromstart!`](@ref) to have that performed automatically.
10001102The default value for `recurse` is `JuliaInterpreter.finish_and_return!`.
10011103`isrequired` pertains only to `frame` itself, not any of its callees.
10021104
1105+ When `recurse::SelectiveEvalController` is specified, the selective evaluation execution
1106+ becomes fully correct. Conversely, with the default `finish_and_return!`, selective
1107+ evaluation may not be necessarily correct for all possible Julia code (see
1108+ https://github.com/JuliaDebug/LoweredCodeUtils.jl/pull/99 for more details).
1109+
1110+ Ensure that the specified `controller` is properly synchronized with `isrequired`.
1111+ Additionally note that, at present, it is not possible to recurse the `controller`.
1112+ In other words, there is no system in place for interprocedural selective evaluation.
1113+
10031114This will return either a `BreakpointRef`, the value obtained from the last executed statement
10041115(if stored to `frame.framedata.ssavlues`), or `nothing`.
10051116Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.
0 commit comments