Skip to content

Commit 0404a6e

Browse files
committed
change the recurse interface to AbstractInterpreter-like interface
Align the `recurse` argument to something like the base Compiler's `AbstractInterpreter` and make JuliaInterpreter routines overloadable properly. This change is quite breaking (thus bumping the minor version of this package), but necessary to enhance the customizability of JI. For example, it will make it easier to add changes like #682 in a nicer way, but also should enable better designs in packages such as Revise and JET.
1 parent 0be2e0d commit 0404a6e

File tree

9 files changed

+266
-234
lines changed

9 files changed

+266
-234
lines changed

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name = "JuliaInterpreter"
22
uuid = "aa1ae85d-cabe-5617-a682-6adf51b2e16a"
3-
version = "0.9.46"
3+
version = "0.10.0"
44

55
[deps]
66
CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2"

docs/src/dev_reference.md

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
@interpret
77
```
88

9+
## Interpreter
10+
11+
```@docs
12+
JuliaInterpreter.Interpreter
13+
JuliaInterpreter.RecursiveInterpreter
14+
JuliaInterpreter.Compiled
15+
```
16+
917
## Frame creation
1018

1119
```@docs
@@ -31,7 +39,6 @@ leaf
3139
## Frame execution
3240

3341
```@docs
34-
JuliaInterpreter.Compiled
3542
JuliaInterpreter.step_expr!
3643
JuliaInterpreter.finish!
3744
JuliaInterpreter.finish_and_return!
@@ -67,7 +74,7 @@ toggle
6774
break_on
6875
break_off
6976
breakpoints
70-
JuliaInterpreter.dummy_breakpoint
77+
JuliaInterpreter.BreakOnCall
7178
```
7279

7380
## Types

src/commands.jl

Lines changed: 138 additions & 129 deletions
Large diffs are not rendered by default.

src/interpret.jl

Lines changed: 45 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ end
8888
# and hence our re-use of the `callargs` field of Frame would introduce
8989
# bugs. Since these nodes use a very limited repertoire of calls, we can special-case
9090
# this quite easily.
91-
function lookup_or_eval(@nospecialize(recurse), frame::Frame, @nospecialize(node))
91+
function lookup_or_eval(interp::Interpreter, frame::Frame, @nospecialize(node))
9292
if isa(node, SSAValue)
9393
return lookup_var(frame, node)
9494
elseif isa(node, SlotNumber)
@@ -102,7 +102,7 @@ function lookup_or_eval(@nospecialize(recurse), frame::Frame, @nospecialize(node
102102
elseif isa(node, Expr)
103103
ex = Expr(node.head)
104104
for arg in node.args
105-
push!(ex.args, lookup_or_eval(recurse, frame, arg))
105+
push!(ex.args, lookup_or_eval(interp, frame, arg))
106106
end
107107
if ex.head === :call
108108
f = ex.args[1]
@@ -136,7 +136,7 @@ function lookup_or_eval(@nospecialize(recurse), frame::Frame, @nospecialize(node
136136
elseif isa(node, Type)
137137
return node
138138
end
139-
return eval_rhs(recurse, frame, node)
139+
return eval_rhs(interp, frame, node)
140140
end
141141

142142
function resolvefc(frame::Frame, @nospecialize(expr))
@@ -159,13 +159,13 @@ function resolvefc(frame::Frame, @nospecialize(expr))
159159
@invokelatest error("unexpected ccall to ", expr)
160160
end
161161

162-
function collect_args(@nospecialize(recurse), frame::Frame, call_expr::Expr; isfc::Bool=false)
162+
function collect_args(interp::Interpreter, frame::Frame, call_expr::Expr; isfc::Bool=false)
163163
args = frame.framedata.callargs
164164
resize!(args, length(call_expr.args))
165165
args[1] = isfc ? resolvefc(frame, call_expr.args[1]) : lookup(frame, call_expr.args[1])
166166
for i = 2:length(args)
167167
if isexpr(call_expr.args[i], :call)
168-
args[i] = lookup_or_eval(recurse, frame, call_expr.args[i])
168+
args[i] = lookup_or_eval(interp, frame, call_expr.args[i])
169169
else
170170
args[i] = lookup(frame, call_expr.args[i])
171171
end
@@ -174,13 +174,13 @@ function collect_args(@nospecialize(recurse), frame::Frame, call_expr::Expr; isf
174174
end
175175

176176
"""
177-
ret = evaluate_foreigncall(recurse, frame::Frame, call_expr)
177+
ret = evaluate_foreigncall(interp, frame::Frame, call_expr)
178178
179179
Evaluate a `:foreigncall` (from a `ccall`) statement `callexpr` in the context of `frame`.
180180
"""
181-
function evaluate_foreigncall(@nospecialize(recurse), frame::Frame, call_expr::Expr)
181+
function evaluate_foreigncall(interp::Interpreter, frame::Frame, call_expr::Expr)
182182
head = call_expr.head
183-
args = collect_args(recurse, frame, call_expr; isfc = head === :foreigncall)
183+
args = collect_args(interp, frame, call_expr; isfc = head === :foreigncall)
184184
for i = 2:length(args)
185185
arg = args[i]
186186
args[i] = isa(arg, Symbol) ? QuoteNode(arg) : arg
@@ -210,11 +210,11 @@ function evaluate_foreigncall(@nospecialize(recurse), frame::Frame, call_expr::E
210210
end
211211

212212
# We have to intercept ccalls / llvmcalls before we try it as a builtin
213-
function bypass_builtins(@nospecialize(recurse), frame::Frame, call_expr::Expr, pc::Int)
213+
function bypass_builtins(interp::Interpreter, frame::Frame, call_expr::Expr, pc::Int)
214214
if isassigned(frame.framecode.methodtables, pc)
215215
tme = frame.framecode.methodtables[pc]
216216
if isa(tme, Compiled)
217-
fargs = collect_args(recurse, frame, call_expr)
217+
fargs = collect_args(interp, frame, call_expr)
218218
f = to_function(fargs[1])
219219
fmod = parentmodule(f)::Module
220220
if fmod === JuliaInterpreter.CompiledCalls || fmod === Core.Compiler
@@ -243,25 +243,25 @@ function native_call(fargs::Vector{Any}, frame::Frame)
243243
return @invokelatest f(fargs...)
244244
end
245245

246-
function evaluate_call_compiled!(::Compiled, frame::Frame, call_expr::Expr; enter_generated::Bool=false)
246+
function evaluate_call!(interp::Compiled, frame::Frame, call_expr::Expr, enter_generated::Bool=false)
247247
# @assert !enter_generated
248248
pc = frame.pc
249-
ret = bypass_builtins(Compiled(), frame, call_expr, pc)
249+
ret = bypass_builtins(interp, frame, call_expr, pc)
250250
isa(ret, Some{Any}) && return ret.value
251251
ret = maybe_evaluate_builtin(frame, call_expr, false)
252252
isa(ret, Some{Any}) && return ret.value
253-
fargs = collect_args(Compiled(), frame, call_expr)
253+
fargs = collect_args(interp, frame, call_expr)
254254
return native_call(fargs, frame)
255255
end
256256

257-
function evaluate_call_recurse!(@nospecialize(recurse), frame::Frame, call_expr::Expr; enter_generated::Bool=false)
257+
function evaluate_call!(interp::Interpreter, frame::Frame, call_expr::Expr, enter_generated::Bool=false)
258258
pc = frame.pc
259-
ret = bypass_builtins(recurse, frame, call_expr, pc)
259+
ret = bypass_builtins(interp, frame, call_expr, pc)
260260
isa(ret, Some{Any}) && return ret.value
261261
ret = maybe_evaluate_builtin(frame, call_expr, true)
262262
isa(ret, Some{Any}) && return ret.value
263263
call_expr = ret
264-
fargs = collect_args(recurse, frame, call_expr)
264+
fargs = collect_args(interp, frame, call_expr)
265265
if fargs[1] === Core.eval
266266
return Core.eval(fargs[2], fargs[3]) # not a builtin, but worth treating specially
267267
elseif fargs[1] === Base.rethrow
@@ -291,30 +291,24 @@ function evaluate_call_recurse!(@nospecialize(recurse), frame::Frame, call_expr:
291291
npc = newframe.pc
292292
shouldbreak(newframe, npc) && return BreakpointRef(newframe.framecode, npc)
293293
# if the following errors, handle_err will pop the stack and recycle newframe
294-
if recurse === finish_and_return!
295-
# Optimize this case to avoid dynamic dispatch
296-
ret = finish_and_return!(finish_and_return!, newframe, false)
297-
else
298-
ret = recurse(recurse, newframe, false)
299-
end
294+
ret = finish_and_return!(interp, newframe, false)
300295
isa(ret, BreakpointRef) && return ret
301296
frame.callee = nothing
302297
return_from(newframe)
303298
return ret
304299
end
305300

306301
"""
307-
ret = evaluate_call!(Compiled(), frame::Frame, call_expr)
308-
ret = evaluate_call!(recurse, frame::Frame, call_expr)
302+
ret = evaluate_call!(interp::Interpreter, frame::Frame, call_expr::Expr, enter_generated::Bool=false)
303+
ret = evaluate_call!(frame::Frame, call_expr::Expr, enter_generated::Bool=false)
309304
310305
Evaluate a `:call` expression `call_expr` in the context of `frame`.
311306
The first causes it to be executed using Julia's normal dispatch (compiled code),
312307
whereas the second recurses in via the interpreter.
313-
`recurse` has a default value of [`JuliaInterpreter.finish_and_return!`](@ref).
308+
`interp` has a default value of [`RecursiveInterpreter`](@ref).
314309
"""
315-
evaluate_call!(::Compiled, frame::Frame, call_expr::Expr; kwargs...) = evaluate_call_compiled!(Compiled(), frame, call_expr; kwargs...)
316-
evaluate_call!(@nospecialize(recurse), frame::Frame, call_expr::Expr; kwargs...) = evaluate_call_recurse!(recurse, frame, call_expr; kwargs...)
317-
evaluate_call!(frame::Frame, call_expr::Expr; kwargs...) = evaluate_call!(finish_and_return!, frame, call_expr; kwargs...)
310+
evaluate_call!(frame::Frame, call_expr::Expr, enter_generated::Bool=false) =
311+
evaluate_call!(RecursiveInterpreter(), frame, call_expr, enter_generated)
318312

319313
# The following come up only when evaluating toplevel code
320314
function evaluate_methoddef(frame::Frame, node::Expr)
@@ -398,7 +392,7 @@ function maybe_assign!(frame::Frame, @nospecialize(stmt), @nospecialize(val))
398392
end
399393
maybe_assign!(frame::Frame, @nospecialize(val)) = maybe_assign!(frame, pc_expr(frame), val)
400394

401-
function eval_rhs(@nospecialize(recurse), frame::Frame, node::Expr)
395+
function eval_rhs(interp::Interpreter, frame::Frame, node::Expr)
402396
head = node.head
403397
if head === :new
404398
args = Any[lookup(frame, arg) for arg in node.args]
@@ -413,11 +407,9 @@ function eval_rhs(@nospecialize(recurse), frame::Frame, node::Expr)
413407
elseif head === :isdefined
414408
return check_isdefined(frame, node.args[1])
415409
elseif head === :call
416-
# here it's crucial to avoid dynamic dispatch
417-
isa(recurse, Compiled) && return evaluate_call_compiled!(recurse, frame, node)
418-
return evaluate_call_recurse!(recurse, frame, node)
410+
return evaluate_call!(interp, frame, node)
419411
elseif head === :foreigncall || head === :cfunction
420-
return evaluate_foreigncall(recurse, frame, node)
412+
return evaluate_foreigncall(interp, frame, node)
421413
elseif head === :copyast
422414
val = (node.args[1]::QuoteNode).value
423415
return isa(val, Expr) ? copy(val) : val
@@ -481,7 +473,7 @@ end
481473
# in `step_expr!`
482474
const _location = Dict{Tuple{Method,Int},Int}()
483475

484-
function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), istoplevel::Bool)
476+
function step_expr!(interp::Interpreter, frame::Frame, @nospecialize(node), istoplevel::Bool)
485477
pc, code, data = frame.pc, frame.framecode, frame.framedata
486478
# if !is_leaf(frame)
487479
# show_stackloc(frame)
@@ -501,7 +493,7 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
501493
if node.head === :(=)
502494
lhs, rhs = node.args
503495
if isa(rhs, Expr)
504-
rhs = eval_rhs(recurse, frame, rhs)
496+
rhs = eval_rhs(interp, frame, rhs)
505497
else
506498
rhs = lookup(frame, rhs)
507499
end
@@ -547,12 +539,12 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
547539
end
548540
elseif node.head === :thunk
549541
newframe = Frame(moduleof(frame), node.args[1]::CodeInfo)
550-
if isa(recurse, Compiled)
551-
finish!(recurse, newframe, true)
542+
if isa(interp, Compiled)
543+
finish!(interp, newframe, true)
552544
else
553545
newframe.caller = frame
554546
frame.callee = newframe
555-
finish!(recurse, newframe, true)
547+
finish!(interp, newframe, true)
556548
frame.callee = nothing
557549
end
558550
return_from(newframe)
@@ -569,7 +561,7 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
569561
end
570562
newframe = ($Frame)(mod, ex)
571563
while true
572-
($through_methoddef_or_done!)($recurse, newframe) === nothing && break
564+
($through_methoddef_or_done!)($interp, newframe) === nothing && break
573565
end
574566
$return_from(newframe)
575567
end)))
@@ -580,12 +572,12 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
580572
elseif node.head === :latestworld
581573
frame.world = Base.get_world_counter()
582574
else
583-
rhs = eval_rhs(recurse, frame, node)
575+
rhs = eval_rhs(interp, frame, node)
584576
end
585577
elseif node.head === :thunk || node.head === :toplevel
586578
error("this frame needs to be run at top level")
587579
else
588-
rhs = eval_rhs(recurse, frame, node)
580+
rhs = eval_rhs(interp, frame, node)
589581
end
590582
elseif isa(node, GotoNode)
591583
@assert is_leaf(frame)
@@ -616,7 +608,7 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
616608
rhs = lookup(frame, node)
617609
end
618610
catch err
619-
return handle_err(recurse, frame, err)
611+
return handle_err(interp, frame, err)
620612
end
621613
@isdefined(rhs) && isa(rhs, BreakpointRef) && return rhs
622614
if isassign(frame, pc)
@@ -631,25 +623,24 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
631623
end
632624

633625
"""
634-
pc = step_expr!(recurse, frame, istoplevel=false)
626+
pc = step_expr!(interp::Interpreter, frame, istoplevel=false)
635627
pc = step_expr!(frame, istoplevel=false)
636628
637629
Execute the next statement in `frame`. `pc` is the new program counter, or `nothing`
638630
if execution terminates, or a [`BreakpointRef`](@ref) if execution hits a breakpoint.
639631
640-
`recurse` controls call evaluation; `recurse = Compiled()` evaluates :call expressions
641-
by normal dispatch. The default value `recurse = finish_and_return!` will use recursive
632+
`interp` controls call evaluation; `interp = Compiled()` evaluates :call expressions
633+
by normal dispatch. The default value `interp = RecursiveInterpreter()` will use recursive
642634
interpretation.
643635
644636
If you are evaluating `frame` at module scope you should pass `istoplevel=true`.
645637
"""
646-
step_expr!(@nospecialize(recurse), frame::Frame, istoplevel::Bool=false) =
647-
step_expr!(recurse, frame, pc_expr(frame), istoplevel)
648-
step_expr!(frame::Frame, istoplevel::Bool=false) =
649-
step_expr!(finish_and_return!, frame, istoplevel)
638+
step_expr!(interp::Interpreter, frame::Frame, istoplevel::Bool=false) =
639+
step_expr!(interp, frame, pc_expr(frame), istoplevel)
640+
step_expr!(frame::Frame, istoplevel::Bool=false) = step_expr!(RecursiveInterpreter(), frame, istoplevel)
650641

651642
"""
652-
loc = handle_err(recurse, frame, err)
643+
loc = handle_err(interp, frame, err)
653644
654645
Deal with an error `err` that arose while evaluating `frame`. There are one of three
655646
behaviors:
@@ -660,7 +651,7 @@ behaviors:
660651
`loc` is a `BreakpointRef`;
661652
- otherwise, `err` gets rethrown.
662653
"""
663-
function handle_err(@nospecialize(recurse), frame::Frame, @nospecialize(err))
654+
function handle_err(::Interpreter, frame::Frame, @nospecialize(err))
664655
data = frame.framedata
665656
err_will_be_thrown_to_top_level = isempty(data.exception_frames) && !data.caller_will_catch_err
666657
if break_on_throw[] || (break_on_error[] && err_will_be_thrown_to_top_level)
@@ -692,15 +683,16 @@ end
692683
lookup_return(frame::Frame, node::ReturnNode) = lookup(frame, node.val)
693684

694685
"""
695-
ret = get_return(frame)
686+
ret = get_return(interp, frame)
696687
697688
Get the return value of `frame`. Throws an error if `frame.pc` does not point to a `return` expression.
698689
`frame` must have already been executed so that the return value has been computed (see,
699690
e.g., [`JuliaInterpreter.finish!`](@ref)).
700691
"""
701-
function get_return(frame)
692+
function get_return(interp::Interpreter, frame::Frame)
702693
node = pc_expr(frame)
703694
is_return(node) || @invokelatest error("expected return statement, got ", node)
704695
return lookup_return(frame, node)
705696
end
697+
get_return(frame::Frame) = get_return(RecursiveInterpreter(), frame)
706698
get_return(t::Tuple{Module,Expr,Frame}) = get_return(t[end])

src/packagedef.jl

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ using Random
1010
using Random.DSFMT
1111
using InteractiveUtils
1212

13-
export @interpret, Compiled, Frame, root, leaf, ExprSplitter,
14-
BreakpointRef, breakpoint, @breakpoint, breakpoints, enable, disable, remove, toggle,
15-
debug_command, @bp, break_on, break_off, on_breakpoints_updated
13+
export BreakpointRef, Compiled, ExprSplitter, Frame, Interpreter, RecursiveInterpreter,
14+
@bp, @breakpoint, @interpret,
15+
break_off, break_on, breakpoint, breakpoints, debug_command, disable, enable, leaf,
16+
on_breakpoints_updated, remove, root, toggle
1617

1718
module CompiledCalls
1819
# This module is for handling intrinsics that must be compiled (llvmcall) as well as ccalls

src/types.jl

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
11
"""
2-
`Compiled` is a trait indicating that any `:call` expressions should be evaluated
3-
using Julia's normal compiled-code evaluation. The alternative is to pass `stack=Frame[]`,
4-
which will cause all calls to be evaluated via the interpreter.
2+
abstract type Interpreter end
3+
4+
The abstract type that all interpreters should subtype.
5+
It defines how JuliaInterpreter behaves when evaluating code.
6+
An interpreter that subtypes this type can implement its own evaluation strategies, by
7+
overloading the certain methods in JuliaInterpreter that are defined for this base type.
8+
The default behaviors of `Interpreter` is same as that of [`RecursiveInterpreter`](@ref),
9+
meaning it will recursively interpret all `:call` expressions.
10+
"""
11+
abstract type Interpreter end
12+
13+
"""
14+
RecursiveInterpreter <: Interpreter
15+
16+
`RecursiveInterpreter` is an [`Interpreter`](@ref) that recurses into any `:call`
17+
expressions in the code being interpreted.
18+
"""
19+
struct RecursiveInterpreter <: Interpreter end
20+
21+
"""
22+
Compiled <: Interpreter
23+
24+
`Compiled` is an [`Interpreter`](@ref) that evaluates any `:call` expressions in the code
25+
being interpreted using Julia's normal compiled-code execution.
526
"""
6-
struct Compiled end
27+
struct Compiled <: Interpreter end
728
Base.similar(::Compiled, sz) = Compiled() # to support similar(stack, 0)
829

930
# Our own replacements for Core types. We need to do this to ensure we can tell the difference

0 commit comments

Comments
 (0)