@@ -183,6 +183,42 @@ function postprint_linelinks(io::IO, idx::Int, src::CodeInfo, cl::CodeLinks, bbc
183
183
return nothing
184
184
end
185
185
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
+
186
222
function namedkeys (cl:: CodeLinks )
187
223
ukeys = Set {GlobalRef} ()
188
224
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
573
609
end
574
610
575
611
"""
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] )
578
614
579
615
Determine which lines might need to be executed to evaluate `obj` or the statement indexed by `idx`.
580
616
If `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
583
619
584
620
See also [`lines_required!`](@ref) and [`selective_eval!`](@ref).
585
621
"""
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... )
587
625
isrequired = falses (length (edges. preds))
588
626
objs = Set {GlobalRef} ([obj])
589
- return lines_required! (isrequired, objs, src, edges; kwargs... )
627
+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
590
628
end
591
629
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... )
593
633
isrequired = falses (length (edges. preds))
594
634
isrequired[idx] = true
595
635
objs = Set {GlobalRef} ()
596
- return lines_required! (isrequired, objs, src, edges; kwargs... )
636
+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
597
637
end
598
638
599
639
"""
600
- lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges;
640
+ lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges,
641
+ [controller::SelectiveEvalController];
601
642
norequire = ())
602
643
603
644
Like `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.
609
650
For example, use `norequire = LoweredCodeUtils.exclude_named_typedefs(src, edges)` if you're
610
651
extracting method signatures and not evaluating new definitions.
611
652
"""
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... )
613
656
objs = Set {GlobalRef} ()
614
- return lines_required! (isrequired, objs, src, edges; kwargs... )
657
+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
615
658
end
616
659
617
660
function exclude_named_typedefs (src:: CodeInfo , edges:: CodeEdges )
@@ -631,7 +674,9 @@ function exclude_named_typedefs(src::CodeInfo, edges::CodeEdges)
631
674
return norequire
632
675
end
633
676
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 = ())
635
680
# Mark any requested objects (their lines of assignment)
636
681
objs = add_requests! (isrequired, objs, edges, norequire)
637
682
@@ -666,7 +711,10 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
666
711
end
667
712
668
713
# 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)
670
718
671
719
return isrequired
672
720
end
@@ -745,13 +793,14 @@ using Core.Compiler: CFG, BasicBlock, compute_basic_blocks
745
793
# The basic algorithm is based on what was proposed in [^Wei84]. If there is even one active
746
794
# block in the blocks reachable from a conditional branch up to its successors' nearest
747
795
# 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
749
797
# from the conditional branch to the nearest common post-dominator.
750
798
#
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`.
755
804
#
756
805
# [Wei84]: M. Weiser, "Program Slicing," IEEE Transactions on Software Engineering, 10, pages 352-357, July 1984.
757
806
function add_control_flow! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
@@ -826,8 +875,8 @@ function reachable_blocks(cfg, from_bb::Int, to_bb::Int)
826
875
return visited
827
876
end
828
877
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 )
831
880
changed = false
832
881
for bbidx = 1 : length (cfg. blocks)
833
882
if bbidx ∉ dead_blocks
@@ -845,7 +894,7 @@ function add_active_gotos!(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
845
894
end
846
895
847
896
# 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 )
849
898
dead_blocks = BitSet ()
850
899
for bbidx = 1 : length (cfg. blocks)
851
900
bb = cfg. blocks[bbidx]
@@ -866,13 +915,31 @@ function compute_dead_blocks(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
866
915
end
867
916
if ! is_𝑰𝑵𝑭𝑳_active
868
917
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
869
923
end
870
924
end
871
925
end
872
926
end
873
927
return dead_blocks
874
928
end
875
929
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
+
876
943
# Do a traveral of "numbered" predecessors and find statement ranges and names of type definitions
877
944
function find_typedefs (src:: CodeInfo )
878
945
typedef_blocks, typedef_names = UnitRange{Int}[], Symbol[]
@@ -995,6 +1062,42 @@ function add_inplace!(isrequired, src, edges, norequire)
995
1062
return changed
996
1063
end
997
1064
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
+
998
1101
"""
999
1102
selective_eval!([recurse], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)
1000
1103
@@ -1007,6 +1110,15 @@ See [`selective_eval_fromstart!`](@ref) to have that performed automatically.
1007
1110
The default value for `recurse` is `JuliaInterpreter.finish_and_return!`.
1008
1111
`isrequired` pertains only to `frame` itself, not any of its callees.
1009
1112
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
+
1010
1122
This will return either a `BreakpointRef`, the value obtained from the last executed statement
1011
1123
(if stored to `frame.framedata.ssavlues`), or `nothing`.
1012
1124
Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.
0 commit comments