@@ -173,6 +173,42 @@ function postprint_linelinks(io::IO, idx::Int, src::CodeInfo, cl::CodeLinks, bbc
173
173
return nothing
174
174
end
175
175
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
+
176
212
function namedkeys (cl:: CodeLinks )
177
213
ukeys = Set {GlobalRef} ()
178
214
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
563
599
end
564
600
565
601
"""
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] )
568
604
569
605
Determine which lines might need to be executed to evaluate `obj` or the statement indexed by `idx`.
570
606
If `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
573
609
574
610
See also [`lines_required!`](@ref) and [`selective_eval!`](@ref).
575
611
"""
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... )
577
615
isrequired = falses (length (edges. preds))
578
616
objs = Set {GlobalRef} ([obj])
579
- return lines_required! (isrequired, objs, src, edges; kwargs... )
617
+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
580
618
end
581
619
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... )
583
623
isrequired = falses (length (edges. preds))
584
624
isrequired[idx] = true
585
625
objs = Set {GlobalRef} ()
586
- return lines_required! (isrequired, objs, src, edges; kwargs... )
626
+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
587
627
end
588
628
589
629
"""
590
- lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges;
630
+ lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges,
631
+ [controller::SelectiveEvalController];
591
632
norequire = ())
592
633
593
634
Like `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.
599
640
For example, use `norequire = LoweredCodeUtils.exclude_named_typedefs(src, edges)` if you're
600
641
extracting method signatures and not evaluating new definitions.
601
642
"""
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... )
603
646
objs = Set {GlobalRef} ()
604
- return lines_required! (isrequired, objs, src, edges; kwargs... )
647
+ return lines_required! (isrequired, objs, src, edges, controller ; kwargs... )
605
648
end
606
649
607
650
function exclude_named_typedefs (src:: CodeInfo , edges:: CodeEdges )
@@ -621,7 +664,9 @@ function exclude_named_typedefs(src::CodeInfo, edges::CodeEdges)
621
664
return norequire
622
665
end
623
666
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 = ())
625
670
# Mark any requested objects (their lines of assignment)
626
671
objs = add_requests! (isrequired, objs, edges, norequire)
627
672
@@ -656,7 +701,10 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
656
701
end
657
702
658
703
# 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)
660
708
661
709
return isrequired
662
710
end
@@ -735,13 +783,14 @@ using Core.Compiler: CFG, BasicBlock, compute_basic_blocks
735
783
# The basic algorithm is based on what was proposed in [^Wei84]. If there is even one active
736
784
# block in the blocks reachable from a conditional branch up to its successors' nearest
737
785
# 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
739
787
# from the conditional branch to the nearest common post-dominator.
740
788
#
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`.
745
794
#
746
795
# [Wei84]: M. Weiser, "Program Slicing," IEEE Transactions on Software Engineering, 10, pages 352-357, July 1984.
747
796
function add_control_flow! (isrequired, src:: CodeInfo , cfg:: CFG , postdomtree)
@@ -816,8 +865,8 @@ function reachable_blocks(cfg, from_bb::Int, to_bb::Int)
816
865
return visited
817
866
end
818
867
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 )
821
870
changed = false
822
871
for bbidx = 1 : length (cfg. blocks)
823
872
if bbidx ∉ dead_blocks
@@ -835,7 +884,7 @@ function add_active_gotos!(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
835
884
end
836
885
837
886
# 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 )
839
888
dead_blocks = BitSet ()
840
889
for bbidx = 1 : length (cfg. blocks)
841
890
bb = cfg. blocks[bbidx]
@@ -856,13 +905,31 @@ function compute_dead_blocks(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
856
905
end
857
906
if ! is_𝑰𝑵𝑭𝑳_active
858
907
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
859
913
end
860
914
end
861
915
end
862
916
end
863
917
return dead_blocks
864
918
end
865
919
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
+
866
933
# Do a traveral of "numbered" predecessors and find statement ranges and names of type definitions
867
934
function find_typedefs (src:: CodeInfo )
868
935
typedef_blocks, typedef_names = UnitRange{Int}[], Symbol[]
@@ -985,6 +1052,42 @@ function add_inplace!(isrequired, src, edges, norequire)
985
1052
return changed
986
1053
end
987
1054
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
+
988
1091
"""
989
1092
selective_eval!([recurse], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)
990
1093
@@ -997,6 +1100,15 @@ See [`selective_eval_fromstart!`](@ref) to have that performed automatically.
997
1100
The default value for `recurse` is `JuliaInterpreter.finish_and_return!`.
998
1101
`isrequired` pertains only to `frame` itself, not any of its callees.
999
1102
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
+
1000
1112
This will return either a `BreakpointRef`, the value obtained from the last executed statement
1001
1113
(if stored to `frame.framedata.ssavlues`), or `nothing`.
1002
1114
Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.
0 commit comments