@@ -182,6 +182,42 @@ function postprint_linelinks(io::IO, idx::Int, src::CodeInfo, cl::CodeLinks, bbc
182
182
return nothing
183
183
end
184
184
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. And if the `return` statement is not
198
+ included in the slice in such cases, it is necessary to terminate `selective_eval!` when
199
+ execution reaches such implicit return statements. `controller.implicit_returns` records
200
+ the PCs of such return statements, and `selective_eval!` will return when reaching those statements.
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{RC}
215
+ inner:: RC # N.B. this doesn't support recursive selective evaluation
216
+ implicit_returns:: BitSet # pc where selective execution should terminate even if they're inactive
217
+ shortcuts:: Vector{CFGShortCut}
218
+ SelectiveEvalController (inner:: RC = finish_and_return!) where RC = new {RC} (inner, BitSet (), CFGShortCut[])
219
+ end
220
+
185
221
function namedkeys (cl:: CodeLinks )
186
222
ukeys = Set {GlobalRef} ()
187
223
for c in (cl. namepreds, cl. namesuccs, cl. nameassigns)
@@ -591,8 +627,8 @@ function terminal_preds!(s, j, edges, covered) # can't be an inner function bec
591
627
end
592
628
593
629
"""
594
- isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges)
595
- isrequired = lines_required(idx::Int, src::CodeInfo, edges::CodeEdges)
630
+ isrequired = lines_required(obj::GlobalRef, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController] )
631
+ isrequired = lines_required(idx::Int, src::CodeInfo, edges::CodeEdges, [controller::SelectiveEvalController] )
596
632
597
633
Determine which lines might need to be executed to evaluate `obj` or the statement indexed by `idx`.
598
634
If `isrequired[i]` is `false`, the `i`th statement is *not* required.
@@ -601,21 +637,26 @@ will end up skipping a subset of such statements, perhaps while repeating others
601
637
602
638
See also [`lines_required!`](@ref) and [`selective_eval!`](@ref).
603
639
"""
604
- function lines_required (obj:: GlobalRef , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
640
+ function lines_required (obj:: GlobalRef , src:: CodeInfo , edges:: CodeEdges ,
641
+ controller:: SelectiveEvalController = SelectiveEvalController ();
642
+ kwargs... )
605
643
isrequired = falses (length (edges. preds))
606
644
objs = Set {GlobalRef} ([obj])
607
- return lines_required! (isrequired, objs, src, edges; kwargs... )
645
+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
608
646
end
609
647
610
- function lines_required (idx:: Int , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
648
+ function lines_required (idx:: Int , src:: CodeInfo , edges:: CodeEdges ,
649
+ controller:: SelectiveEvalController = SelectiveEvalController ();
650
+ kwargs... )
611
651
isrequired = falses (length (edges. preds))
612
652
isrequired[idx] = true
613
653
objs = Set {GlobalRef} ()
614
- return lines_required! (isrequired, objs, src, edges; kwargs... )
654
+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
615
655
end
616
656
617
657
"""
618
- lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges;
658
+ lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges,
659
+ [controller::SelectiveEvalController];
619
660
norequire = ())
620
661
621
662
Like `lines_required`, but where `isrequired[idx]` has already been set to `true` for all statements
@@ -627,9 +668,11 @@ should _not_ be marked as a requirement.
627
668
For example, use `norequire = LoweredCodeUtils.exclude_named_typedefs(src, edges)` if you're
628
669
extracting method signatures and not evaluating new definitions.
629
670
"""
630
- function lines_required! (isrequired:: AbstractVector{Bool} , src:: CodeInfo , edges:: CodeEdges ; kwargs... )
671
+ function lines_required! (isrequired:: AbstractVector{Bool} , src:: CodeInfo , edges:: CodeEdges ,
672
+ controller:: SelectiveEvalController = SelectiveEvalController ();
673
+ kwargs... )
631
674
objs = Set {GlobalRef} ()
632
- return lines_required! (isrequired, objs, src, edges; kwargs... )
675
+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
633
676
end
634
677
635
678
function exclude_named_typedefs (src:: CodeInfo , edges:: CodeEdges )
@@ -649,7 +692,9 @@ function exclude_named_typedefs(src::CodeInfo, edges::CodeEdges)
649
692
return norequire
650
693
end
651
694
652
- function lines_required! (isrequired:: AbstractVector{Bool} , objs, src:: CodeInfo , edges:: CodeEdges ; norequire = ())
695
+ function lines_required! (isrequired:: AbstractVector{Bool} , objs, src:: CodeInfo , edges:: CodeEdges ,
696
+ controller:: SelectiveEvalController = SelectiveEvalController ();
697
+ norequire = ())
653
698
# Mark any requested objects (their lines of assignment)
654
699
objs = add_requests! (isrequired, objs, edges, norequire)
655
700
@@ -684,7 +729,10 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
684
729
end
685
730
686
731
# now mark the active goto nodes
687
- add_active_gotos! (isrequired, src, cfg, postdomtree)
732
+ add_active_gotos! (isrequired, src, cfg, postdomtree, controller)
733
+
734
+ # check if there are any implicit return blocks
735
+ record_implcit_return! (controller, isrequired, cfg)
688
736
689
737
return isrequired
690
738
end
@@ -768,13 +816,14 @@ end
768
816
# The basic algorithm is based on what was proposed in [^Wei84]. If there is even one active
769
817
# block in the blocks reachable from a conditional branch up to its successors' nearest
770
818
# common post-dominator (referred to as 𝑰𝑵𝑭𝑳 in the paper), it is necessary to follow
771
- # that conditional branch and execute the code. Otherwise, execution can be short-circuited
819
+ # that conditional branch and execute the code. Otherwise, execution can be short-cut
772
820
# from the conditional branch to the nearest common post-dominator.
773
821
#
774
- # COMBAK: It is important to note that in Julia's intermediate code representation (`CodeInfo`),
775
- # "short-circuiting" a specific code region is not a simple task. Simply ignoring the path
776
- # to the post-dominator does not guarantee fall-through to the post-dominator. Therefore,
777
- # a more careful implementation is required for this aspect.
822
+ # It is important to note that in Julia's intermediate code representation (`CodeInfo`),
823
+ # "short-cutting" a specific code region is not a simple task. Simply incrementing the
824
+ # program counter without executing the statements of 𝑰𝑵𝑭𝑳 blocks does not guarantee that
825
+ # the program counter fall-throughs to the post-dominator.
826
+ # To handle such cases, `selective_eval!` needs to use `SelectiveEvalController`.
778
827
#
779
828
# [Wei84]: M. Weiser, "Program Slicing," IEEE Transactions on Software Engineering, 10, pages 352-357, July 1984.
780
829
function add_control_flow! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
@@ -849,8 +898,8 @@ function reachable_blocks(cfg, from_bb::Int, to_bb::Int)
849
898
return visited
850
899
end
851
900
852
- function add_active_gotos! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
853
- dead_blocks = compute_dead_blocks (isrequired, src, cfg, postdomtree)
901
+ function add_active_gotos! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree, controller :: SelectiveEvalController )
902
+ dead_blocks = compute_dead_blocks! (isrequired, src, cfg, postdomtree, controller )
854
903
changed = false
855
904
for bbidx = 1 : length (cfg. blocks)
856
905
if bbidx ∉ dead_blocks
@@ -868,7 +917,7 @@ function add_active_gotos!(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
868
917
end
869
918
870
919
# find dead blocks using the same approach as `add_control_flow!`, for the converged `isrequired`
871
- function compute_dead_blocks (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
920
+ function compute_dead_blocks! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree, controller :: SelectiveEvalController )
872
921
dead_blocks = BitSet ()
873
922
for bbidx = 1 : length (cfg. blocks)
874
923
bb = cfg. blocks[bbidx]
@@ -889,13 +938,31 @@ function compute_dead_blocks(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
889
938
end
890
939
if ! is_𝑰𝑵𝑭𝑳_active
891
940
union! (dead_blocks, delete! (𝑰𝑵𝑭𝑳, postdominator))
941
+ if postdominator ≠ 0
942
+ postdominator_bb = cfg. blocks[postdominator]
943
+ postdominator_entryidx = postdominator_bb. stmts[begin ]
944
+ push! (controller. shortcuts, CFGShortCut (termidx, postdominator_entryidx))
945
+ end
892
946
end
893
947
end
894
948
end
895
949
end
896
950
return dead_blocks
897
951
end
898
952
953
+ function record_implcit_return! (controller:: SelectiveEvalController , isrequired, cfg:: CFG )
954
+ for bbidx = 1 : length (cfg. blocks)
955
+ bb = cfg. blocks[bbidx]
956
+ if isempty (bb. succs)
957
+ i = findfirst (idx:: Int -> ! isrequired[idx], bb. stmts)
958
+ if ! isnothing (i)
959
+ push! (controller. implicit_returns, bb. stmts[i])
960
+ end
961
+ end
962
+ end
963
+ nothing
964
+ end
965
+
899
966
# Do a traveral of "numbered" predecessors and find statement ranges and names of type definitions
900
967
function find_typedefs (src:: CodeInfo )
901
968
typedef_blocks, typedef_names = UnitRange{Int}[], Symbol[]
@@ -1018,6 +1085,42 @@ function add_inplace!(isrequired, src, edges, norequire)
1018
1085
return changed
1019
1086
end
1020
1087
1088
+ function JuliaInterpreter. step_expr! (controller:: SelectiveEvalController , frame:: Frame , @nospecialize (node), istoplevel:: Bool )
1089
+ if frame. pc in controller. implicit_returns
1090
+ return nothing
1091
+ elseif node isa GotoIfNot
1092
+ for shortcut in controller. shortcuts
1093
+ if shortcut. from == frame. pc
1094
+ return frame. pc = shortcut. to
1095
+ end
1096
+ end
1097
+ end
1098
+ # TODO allow recursion: @invoke JuliaInterpreter.step_expr!(controller::Any, frame::Frame, node::Any, istoplevel::Bool)
1099
+ JuliaInterpreter. step_expr! (controller. inner, frame, node, istoplevel)
1100
+ end
1101
+
1102
+ next_or_nothing! (frame:: Frame ) = next_or_nothing! (finish_and_return!, frame)
1103
+ function next_or_nothing! (@nospecialize (recurse), frame:: Frame )
1104
+ pc = frame. pc
1105
+ if pc < nstatements (frame. framecode)
1106
+ return frame. pc = pc + 1
1107
+ end
1108
+ return nothing
1109
+ end
1110
+ function next_or_nothing! (controller:: SelectiveEvalController , frame:: Frame )
1111
+ if frame. pc in controller. implicit_returns
1112
+ return nothing
1113
+ elseif pc_expr (frame) isa GotoIfNot
1114
+ for shortcut in controller. shortcuts
1115
+ if shortcut. from == frame. pc
1116
+ return frame. pc = shortcut. to
1117
+ end
1118
+ end
1119
+ end
1120
+ # TODO allow recursion: @invoke next_or_nothing!(controller::Any, frame::Frame)
1121
+ next_or_nothing! (controller. inner, frame)
1122
+ end
1123
+
1021
1124
"""
1022
1125
struct SelectiveInterpreter{S<:Interpreter,T<:AbstractVector{Bool}} <: Interpreter
1023
1126
inner::S
@@ -1068,6 +1171,15 @@ See [`selective_eval_fromstart!`](@ref) to have that performed automatically.
1068
1171
1069
1172
`isrequired` pertains only to `frame` itself, not any of its callees.
1070
1173
1174
+ When `recurse::SelectiveEvalController` is specified, the selective evaluation execution
1175
+ becomes fully correct. Conversely, with the default `finish_and_return!`, selective
1176
+ evaluation may not be necessarily correct for all possible Julia code (see
1177
+ https://github.com/JuliaDebug/LoweredCodeUtils.jl/pull/99 for more details).
1178
+
1179
+ Ensure that the specified `controller` is properly synchronized with `isrequired`.
1180
+ Additionally note that, at present, it is not possible to recurse the `controller`.
1181
+ In other words, there is no system in place for interprocedural selective evaluation.
1182
+
1071
1183
This will return either a `BreakpointRef`, the value obtained from the last executed statement
1072
1184
(if stored to `frame.framedata.ssavlues`), or `nothing`.
1073
1185
Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.
0 commit comments