@@ -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