Skip to content

Commit aa4e09d

Browse files
committed
proper termination, take 2
This PR is an alternative to #99. This is built on top of #116. With this PR, the following test cases now pass correctly: ```julia # Final block is not a `return`: Need to use `config::SelectiveEvalRecurse` explicitly ex = quote x = 1 yy = 7 @Label loop x += 1 x < 5 || return yy @goto loop end frame = Frame(ModSelective, ex) src = frame.framecode.src edges = CodeEdges(ModSelective, src) config = SelectiveEvalRecurse() isrequired = lines_required(GlobalRef(ModSelective, :x), src, edges, config) selective_eval_fromstart!(config, frame, isrequired, true) @test ModSelective.x == 5 @test !isdefined(ModSelective, :yy) ``` The basic approach is overloading `JuliaInterpreter.step_expr!` and `LoweredCodeUtils.next_or_nothing!` for the new `SelectiveEvalController` type, as described below, to perform correct selective execution. When `SelectiveEvalController` is passed as the `recurse` argument of `selective_eval!`, the selective execution is adjusted as follows: - **Implicit return**: In Julia's IR representation (`CodeInfo`), the final block does not necessarily return and may `goto` another block. And if the `return` statement is not included in the slice in such cases, it is necessary to terminate `selective_eval!` when execution reaches such implicit return statements. `controller.implicit_returns` records the PCs of such return statements, and `selective_eval!` will return when reaching those statements. This is the core part of the fix for the test cases in #99. - **CFG short-cut**: When the successors of a conditional branch are inactive, and it is safe to move the program counter from the conditional branch to the nearest common post-dominator of those successors, this short-cut is taken. This short-cut is not merely an optimization but is actually essential for the correctness of the selective execution. This is because, in `CodeInfo`, even if we simply fall-through dead blocks (i.e., increment the program counter without executing the statements of those blocks), it does not necessarily lead to the nearest common post-dominator block. And now [`lines_required`](@ref) or [`lines_required!`](@ref) will update the `SelectiveEvalController` passed as their argument to be appropriate for the program slice generated. One thing to note is that currently, the `controller` is not be recursed. That said, in Revise, which is the main consumer of LCU, there is no need for recursive selective execution, and so `selective_eval!` does not provide a system for inter-procedural selective evaluation. Accordingly `SelectiveEvalController` does not recurse too, but this can be left as a future extension.
1 parent 4a85962 commit aa4e09d

8 files changed

+163
-36
lines changed

Project.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ version = "3.0.3"
77
JuliaInterpreter = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
88

99
[compat]
10-
JuliaInterpreter = "0.9"
10+
JuliaInterpreter = "0.9.36"
1111
julia = "1.6"
1212

1313
[extras]

docs/src/api.md

+1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ lines_required
1717
lines_required!
1818
selective_eval!
1919
selective_eval_fromstart!
20+
SelectiveEvalController
2021
```
2122

2223
## Internal utilities

src/LoweredCodeUtils.jl

+4-3
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ module LoweredCodeUtils
1111

1212
using JuliaInterpreter
1313
using JuliaInterpreter: SSAValue, SlotNumber, Frame
14-
using JuliaInterpreter: @lookup, moduleof, pc_expr, step_expr!, is_global_ref, is_quotenode_egal, whichtt,
15-
next_until!, finish_and_return!, get_return, nstatements, codelocation, linetable,
16-
is_return, lookup_return
14+
using JuliaInterpreter: @lookup, moduleof, pc_expr, is_global_ref, is_quotenode_egal,
15+
whichtt, next_until!, finish_and_return!, nstatements, codelocation,
16+
linetable, is_return, lookup_return
17+
import JuliaInterpreter: step_expr!
1718

1819
include("packagedef.jl")
1920

src/codeedges.jl

+131-19
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,42 @@ function postprint_linelinks(io::IO, idx::Int, src::CodeInfo, cl::CodeLinks, bbc
183183
return nothing
184184
end
185185

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+
186222
function namedkeys(cl::CodeLinks)
187223
ukeys = Set{GlobalRef}()
188224
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
573609
end
574610

575611
"""
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])
578614
579615
Determine which lines might need to be executed to evaluate `obj` or the statement indexed by `idx`.
580616
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
583619
584620
See also [`lines_required!`](@ref) and [`selective_eval!`](@ref).
585621
"""
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...)
587625
isrequired = falses(length(edges.preds))
588626
objs = Set{GlobalRef}([obj])
589-
return lines_required!(isrequired, objs, src, edges; kwargs...)
627+
return lines_required!(isrequired, objs, src, edges, controller; kwargs...)
590628
end
591629

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...)
593633
isrequired = falses(length(edges.preds))
594634
isrequired[idx] = true
595635
objs = Set{GlobalRef}()
596-
return lines_required!(isrequired, objs, src, edges; kwargs...)
636+
return lines_required!(isrequired, objs, src, edges, controller; kwargs...)
597637
end
598638

599639
"""
600-
lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges;
640+
lines_required!(isrequired::AbstractVector{Bool}, src::CodeInfo, edges::CodeEdges,
641+
[controller::SelectiveEvalController];
601642
norequire = ())
602643
603644
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.
609650
For example, use `norequire = LoweredCodeUtils.exclude_named_typedefs(src, edges)` if you're
610651
extracting method signatures and not evaluating new definitions.
611652
"""
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...)
613656
objs = Set{GlobalRef}()
614-
return lines_required!(isrequired, objs, src, edges; kwargs...)
657+
return lines_required!(isrequired, objs, src, edges, controller; kwargs...)
615658
end
616659

617660
function exclude_named_typedefs(src::CodeInfo, edges::CodeEdges)
@@ -631,7 +674,9 @@ function exclude_named_typedefs(src::CodeInfo, edges::CodeEdges)
631674
return norequire
632675
end
633676

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 = ())
635680
# Mark any requested objects (their lines of assignment)
636681
objs = add_requests!(isrequired, objs, edges, norequire)
637682

@@ -666,7 +711,10 @@ function lines_required!(isrequired::AbstractVector{Bool}, objs, src::CodeInfo,
666711
end
667712

668713
# 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)
670718

671719
return isrequired
672720
end
@@ -745,13 +793,14 @@ using Core.Compiler: CFG, BasicBlock, compute_basic_blocks
745793
# The basic algorithm is based on what was proposed in [^Wei84]. If there is even one active
746794
# block in the blocks reachable from a conditional branch up to its successors' nearest
747795
# 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
749797
# from the conditional branch to the nearest common post-dominator.
750798
#
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`.
755804
#
756805
# [Wei84]: M. Weiser, "Program Slicing," IEEE Transactions on Software Engineering, 10, pages 352-357, July 1984.
757806
function add_control_flow!(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
@@ -826,8 +875,8 @@ function reachable_blocks(cfg, from_bb::Int, to_bb::Int)
826875
return visited
827876
end
828877

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)
831880
changed = false
832881
for bbidx = 1:length(cfg.blocks)
833882
if bbidx dead_blocks
@@ -845,7 +894,7 @@ function add_active_gotos!(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
845894
end
846895

847896
# 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)
849898
dead_blocks = BitSet()
850899
for bbidx = 1:length(cfg.blocks)
851900
bb = cfg.blocks[bbidx]
@@ -866,13 +915,31 @@ function compute_dead_blocks(isrequired, src::CodeInfo, cfg::CFG, postdomtree)
866915
end
867916
if !is_𝑰𝑵𝑭𝑳_active
868917
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
869923
end
870924
end
871925
end
872926
end
873927
return dead_blocks
874928
end
875929

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+
876943
# Do a traveral of "numbered" predecessors and find statement ranges and names of type definitions
877944
function find_typedefs(src::CodeInfo)
878945
typedef_blocks, typedef_names = UnitRange{Int}[], Symbol[]
@@ -995,6 +1062,42 @@ function add_inplace!(isrequired, src, edges, norequire)
9951062
return changed
9961063
end
9971064

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+
9981101
"""
9991102
selective_eval!([recurse], frame::Frame, isrequired::AbstractVector{Bool}, istoplevel=false)
10001103
@@ -1007,6 +1110,15 @@ See [`selective_eval_fromstart!`](@ref) to have that performed automatically.
10071110
The default value for `recurse` is `JuliaInterpreter.finish_and_return!`.
10081111
`isrequired` pertains only to `frame` itself, not any of its callees.
10091112
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+
10101122
This will return either a `BreakpointRef`, the value obtained from the last executed statement
10111123
(if stored to `frame.framedata.ssavlues`), or `nothing`.
10121124
Typically, assignment to a variable binding does not result in an ssa store by JuliaInterpreter.

src/packagedef.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ const trackedheads = (:method,)
1212
const structdecls = (:_structtype, :_abstracttype, :_primitivetype)
1313

1414
export signature, rename_framemethods!, methoddef!, methoddefs!, bodymethod
15-
export CodeEdges, lines_required, lines_required!, selective_eval!, selective_eval_fromstart!
15+
export CodeEdges, SelectiveEvalController,
16+
lines_required, lines_required!, selective_eval!, selective_eval_fromstart!
1617

1718
include("utils.jl")
1819
include("signatures.jl")

src/signatures.jl

+3-10
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ function rename_framemethods!(@nospecialize(recurse), frame::Frame, methodinfos,
326326
set_to_running_name!(recurse, replacements, frame, methodinfos, selfcalls[idx], calledby, callee, caller)
327327
catch err
328328
@warn "skipping callee $callee (called by $caller) due to $err"
329+
# showerror(stderr, err, stacktrace(catch_backtrace()))
329330
end
330331
end
331332
for sc in selfcalls
@@ -468,16 +469,8 @@ end
468469
Advance the program counter without executing the corresponding line.
469470
If `frame` is finished, `nextpc` will be `nothing`.
470471
"""
471-
next_or_nothing(frame, pc) = next_or_nothing(finish_and_return!, frame, pc)
472-
next_or_nothing(@nospecialize(recurse), frame, pc) = pc < nstatements(frame.framecode) ? pc+1 : nothing
473-
next_or_nothing!(frame) = next_or_nothing!(finish_and_return!, frame)
474-
function next_or_nothing!(@nospecialize(recurse), frame)
475-
pc = frame.pc
476-
if pc < nstatements(frame.framecode)
477-
return frame.pc = pc + 1
478-
end
479-
return nothing
480-
end
472+
next_or_nothing(frame::Frame, pc::Int) = next_or_nothing(finish_and_return!, frame, pc)
473+
next_or_nothing(@nospecialize(recurse), frame::Frame, pc::Int) = pc < nstatements(frame.framecode) ? pc+1 : nothing
481474

482475
"""
483476
nextpc = skip_until(predicate, [recurse], frame, pc)

test/codeedges.jl

+19-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ module ModSelective end
6565
# Check that the result of direct evaluation agrees with selective evaluation
6666
Core.eval(ModEval, ex)
6767
isrequired = lines_required(GlobalRef(ModSelective, :x), src, edges)
68-
# theere is too much diversity in lowering across Julia versions to make it useful to test `sum(isrequired)`
68+
# there is too much diversity in lowering across Julia versions to make it useful to test `sum(isrequired)`
6969
selective_eval_fromstart!(frame, isrequired)
7070
@test ModSelective.x === ModEval.x
7171
@test allmissing(ModSelective, (:y, :z, :a, :b, :k))
@@ -216,6 +216,24 @@ module ModSelective end
216216
@test ModSelective.k11 == 0
217217
@test 3 <= ModSelective.s11 <= 15
218218

219+
# Final block is not a `return`: Need to use `controller::SelectiveEvalController` explicitly
220+
ex = quote
221+
x = 1
222+
yy = 7
223+
@label loop
224+
x += 1
225+
x < 5 || return yy
226+
@goto loop
227+
end
228+
frame = Frame(ModSelective, ex)
229+
src = frame.framecode.src
230+
edges = CodeEdges(ModSelective, src)
231+
controller = SelectiveEvalController()
232+
isrequired = lines_required(GlobalRef(ModSelective, :x), src, edges, controller)
233+
selective_eval_fromstart!(controller, frame, isrequired, true)
234+
@test ModSelective.x == 5
235+
@test !isdefined(ModSelective, :yy)
236+
219237
# Control-flow in an abstract type definition
220238
ex = :(abstract type StructParent{T, N} <: AbstractArray{T, N} end)
221239
frame = Frame(ModSelective, ex)

test/signatures.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -415,9 +415,10 @@ bodymethtest5(x, y=Dict(1=>2)) = 5
415415
oldenv = Pkg.project().path
416416
try
417417
# we test with the old version of CBinding, let's do it in an isolated environment
418+
# so we don't cause package conflicts with everything else
418419
Pkg.activate(; temp=true, io=devnull)
419420

420-
@info "Adding CBinding to the environment for test purposes"
421+
@info "Adding CBinding v0.9.4 to the environment for test purposes"
421422
Pkg.add(; name="CBinding", version="0.9.4", io=devnull) # `@cstruct` isn't defined for v1.0 and above
422423

423424
m = Module()

0 commit comments

Comments
 (0)