Skip to content

Commit d26f140

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 77effa3 commit d26f140

File tree

8 files changed

+248
-232
lines changed

8 files changed

+248
-232
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.44"
3+
version = "0.10.0"
44

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

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
@@ -82,7 +82,7 @@ end
8282
# and hence our re-use of the `callargs` field of Frame would introduce
8383
# bugs. Since these nodes use a very limited repertoire of calls, we can special-case
8484
# this quite easily.
85-
function lookup_or_eval(@nospecialize(recurse), frame::Frame, @nospecialize(node))
85+
function lookup_or_eval(interp::Interpreter, frame::Frame, @nospecialize(node))
8686
if isa(node, SSAValue)
8787
return lookup_var(frame, node)
8888
elseif isa(node, SlotNumber)
@@ -96,7 +96,7 @@ function lookup_or_eval(@nospecialize(recurse), frame::Frame, @nospecialize(node
9696
elseif isa(node, Expr)
9797
ex = Expr(node.head)
9898
for arg in node.args
99-
push!(ex.args, lookup_or_eval(recurse, frame, arg))
99+
push!(ex.args, lookup_or_eval(interp, frame, arg))
100100
end
101101
if ex.head === :call
102102
f = ex.args[1]
@@ -130,7 +130,7 @@ function lookup_or_eval(@nospecialize(recurse), frame::Frame, @nospecialize(node
130130
elseif isa(node, Type)
131131
return node
132132
end
133-
return eval_rhs(recurse, frame, node)
133+
return eval_rhs(interp, frame, node)
134134
end
135135

136136
function resolvefc(frame::Frame, @nospecialize(expr))
@@ -153,14 +153,14 @@ function resolvefc(frame::Frame, @nospecialize(expr))
153153
@invokelatest error("unexpected ccall to ", expr)
154154
end
155155

156-
function collect_args(@nospecialize(recurse), frame::Frame, call_expr::Expr; isfc::Bool=false)
156+
function collect_args(interp::Interpreter, frame::Frame, call_expr::Expr; isfc::Bool=false)
157157
args = frame.framedata.callargs
158158
resize!(args, length(call_expr.args))
159159
mod = moduleof(frame)
160160
args[1] = isfc ? resolvefc(frame, call_expr.args[1]) : @lookup(mod, frame, call_expr.args[1])
161161
for i = 2:length(args)
162162
if isexpr(call_expr.args[i], :call)
163-
args[i] = lookup_or_eval(recurse, frame, call_expr.args[i])
163+
args[i] = lookup_or_eval(interp, frame, call_expr.args[i])
164164
else
165165
args[i] = @lookup(mod, frame, call_expr.args[i])
166166
end
@@ -169,13 +169,13 @@ function collect_args(@nospecialize(recurse), frame::Frame, call_expr::Expr; isf
169169
end
170170

171171
"""
172-
ret = evaluate_foreigncall(recurse, frame::Frame, call_expr)
172+
ret = evaluate_foreigncall(interp, frame::Frame, call_expr)
173173
174174
Evaluate a `:foreigncall` (from a `ccall`) statement `callexpr` in the context of `frame`.
175175
"""
176-
function evaluate_foreigncall(@nospecialize(recurse), frame::Frame, call_expr::Expr)
176+
function evaluate_foreigncall(interp::Interpreter, frame::Frame, call_expr::Expr)
177177
head = call_expr.head
178-
args = collect_args(recurse, frame, call_expr; isfc = head === :foreigncall)
178+
args = collect_args(interp, frame, call_expr; isfc = head === :foreigncall)
179179
for i = 2:length(args)
180180
arg = args[i]
181181
args[i] = isa(arg, Symbol) ? QuoteNode(arg) : arg
@@ -205,11 +205,11 @@ function evaluate_foreigncall(@nospecialize(recurse), frame::Frame, call_expr::E
205205
end
206206

207207
# We have to intercept ccalls / llvmcalls before we try it as a builtin
208-
function bypass_builtins(@nospecialize(recurse), frame::Frame, call_expr::Expr, pc::Int)
208+
function bypass_builtins(interp::Interpreter, frame::Frame, call_expr::Expr, pc::Int)
209209
if isassigned(frame.framecode.methodtables, pc)
210210
tme = frame.framecode.methodtables[pc]
211211
if isa(tme, Compiled)
212-
fargs = collect_args(recurse, frame, call_expr)
212+
fargs = collect_args(interp, frame, call_expr)
213213
f = to_function(fargs[1])
214214
fmod = parentmodule(f)::Module
215215
if fmod === JuliaInterpreter.CompiledCalls || fmod === Core.Compiler
@@ -238,25 +238,25 @@ function native_call(fargs::Vector{Any}, frame::Frame)
238238
return @invokelatest f(fargs...)
239239
end
240240

241-
function evaluate_call_compiled!(::Compiled, frame::Frame, call_expr::Expr; enter_generated::Bool=false)
241+
function evaluate_call!(interp::Compiled, frame::Frame, call_expr::Expr, enter_generated::Bool=false)
242242
# @assert !enter_generated
243243
pc = frame.pc
244-
ret = bypass_builtins(Compiled(), frame, call_expr, pc)
244+
ret = bypass_builtins(interp, frame, call_expr, pc)
245245
isa(ret, Some{Any}) && return ret.value
246246
ret = maybe_evaluate_builtin(frame, call_expr, false)
247247
isa(ret, Some{Any}) && return ret.value
248-
fargs = collect_args(Compiled(), frame, call_expr)
248+
fargs = collect_args(interp, frame, call_expr)
249249
return native_call(fargs, frame)
250250
end
251251

252-
function evaluate_call_recurse!(@nospecialize(recurse), frame::Frame, call_expr::Expr; enter_generated::Bool=false)
252+
function evaluate_call!(interp::Interpreter, frame::Frame, call_expr::Expr, enter_generated::Bool=false)
253253
pc = frame.pc
254-
ret = bypass_builtins(recurse, frame, call_expr, pc)
254+
ret = bypass_builtins(interp, frame, call_expr, pc)
255255
isa(ret, Some{Any}) && return ret.value
256256
ret = maybe_evaluate_builtin(frame, call_expr, true)
257257
isa(ret, Some{Any}) && return ret.value
258258
call_expr = ret
259-
fargs = collect_args(recurse, frame, call_expr)
259+
fargs = collect_args(interp, frame, call_expr)
260260
if fargs[1] === Core.eval
261261
return Core.eval(fargs[2], fargs[3]) # not a builtin, but worth treating specially
262262
elseif fargs[1] === Base.rethrow
@@ -286,30 +286,24 @@ function evaluate_call_recurse!(@nospecialize(recurse), frame::Frame, call_expr:
286286
npc = newframe.pc
287287
shouldbreak(newframe, npc) && return BreakpointRef(newframe.framecode, npc)
288288
# if the following errors, handle_err will pop the stack and recycle newframe
289-
if recurse === finish_and_return!
290-
# Optimize this case to avoid dynamic dispatch
291-
ret = finish_and_return!(finish_and_return!, newframe, false)
292-
else
293-
ret = recurse(recurse, newframe, false)
294-
end
289+
ret = finish_and_return!(interp, newframe, false)
295290
isa(ret, BreakpointRef) && return ret
296291
frame.callee = nothing
297292
return_from(newframe)
298293
return ret
299294
end
300295

301296
"""
302-
ret = evaluate_call!(Compiled(), frame::Frame, call_expr)
303-
ret = evaluate_call!(recurse, frame::Frame, call_expr)
297+
ret = evaluate_call!(interp::Interpreter, frame::Frame, call_expr::Expr, enter_generated::Bool=false)
298+
ret = evaluate_call!(frame::Frame, call_expr::Expr, enter_generated::Bool=false)
304299
305300
Evaluate a `:call` expression `call_expr` in the context of `frame`.
306301
The first causes it to be executed using Julia's normal dispatch (compiled code),
307302
whereas the second recurses in via the interpreter.
308-
`recurse` has a default value of [`JuliaInterpreter.finish_and_return!`](@ref).
303+
`interp` has a default value of [`RecursiveInterpreter`](@ref).
309304
"""
310-
evaluate_call!(::Compiled, frame::Frame, call_expr::Expr; kwargs...) = evaluate_call_compiled!(Compiled(), frame, call_expr; kwargs...)
311-
evaluate_call!(@nospecialize(recurse), frame::Frame, call_expr::Expr; kwargs...) = evaluate_call_recurse!(recurse, frame, call_expr; kwargs...)
312-
evaluate_call!(frame::Frame, call_expr::Expr; kwargs...) = evaluate_call!(finish_and_return!, frame, call_expr; kwargs...)
305+
evaluate_call!(frame::Frame, call_expr::Expr, enter_generated::Bool=false) =
306+
evaluate_call!(RecursiveInterpreter(), frame, call_expr, enter_generated)
313307

314308
# The following come up only when evaluating toplevel code
315309
function evaluate_methoddef(frame::Frame, node::Expr)
@@ -366,7 +360,7 @@ function maybe_assign!(frame::Frame, @nospecialize(stmt), @nospecialize(val))
366360
end
367361
maybe_assign!(frame::Frame, @nospecialize(val)) = maybe_assign!(frame, pc_expr(frame), val)
368362

369-
function eval_rhs(@nospecialize(recurse), frame::Frame, node::Expr)
363+
function eval_rhs(interp::Interpreter, frame::Frame, node::Expr)
370364
head = node.head
371365
if head === :new
372366
mod = moduleof(frame)
@@ -385,11 +379,9 @@ function eval_rhs(@nospecialize(recurse), frame::Frame, node::Expr)
385379
elseif head === :isdefined
386380
return check_isdefined(frame, node.args[1])
387381
elseif head === :call
388-
# here it's crucial to avoid dynamic dispatch
389-
isa(recurse, Compiled) && return evaluate_call_compiled!(recurse, frame, node)
390-
return evaluate_call_recurse!(recurse, frame, node)
382+
return evaluate_call!(interp, frame, node)
391383
elseif head === :foreigncall || head === :cfunction
392-
return evaluate_foreigncall(recurse, frame, node)
384+
return evaluate_foreigncall(interp, frame, node)
393385
elseif head === :copyast
394386
val = (node.args[1]::QuoteNode).value
395387
return isa(val, Expr) ? copy(val) : val
@@ -453,7 +445,7 @@ end
453445
# in `step_expr!`
454446
const _location = Dict{Tuple{Method,Int},Int}()
455447

456-
function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), istoplevel::Bool)
448+
function step_expr!(interp::Interpreter, frame::Frame, @nospecialize(node), istoplevel::Bool)
457449
pc, code, data = frame.pc, frame.framecode, frame.framedata
458450
# if !is_leaf(frame)
459451
# show_stackloc(frame)
@@ -473,7 +465,7 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
473465
if node.head === :(=)
474466
lhs, rhs = node.args
475467
if isa(rhs, Expr)
476-
rhs = eval_rhs(recurse, frame, rhs)
468+
rhs = eval_rhs(interp, frame, rhs)
477469
else
478470
rhs = istoplevel ? @lookup(moduleof(frame), frame, rhs) : @lookup(frame, rhs)
479471
end
@@ -519,12 +511,12 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
519511
end
520512
elseif node.head === :thunk
521513
newframe = Frame(moduleof(frame), node.args[1]::CodeInfo)
522-
if isa(recurse, Compiled)
523-
finish!(recurse, newframe, true)
514+
if isa(interp, Compiled)
515+
finish!(interp, newframe, true)
524516
else
525517
newframe.caller = frame
526518
frame.callee = newframe
527-
finish!(recurse, newframe, true)
519+
finish!(interp, newframe, true)
528520
frame.callee = nothing
529521
end
530522
return_from(newframe)
@@ -541,7 +533,7 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
541533
end
542534
newframe = ($Frame)(mod, ex)
543535
while true
544-
($through_methoddef_or_done!)($recurse, newframe) === nothing && break
536+
($through_methoddef_or_done!)($interp, newframe) === nothing && break
545537
end
546538
$return_from(newframe)
547539
end)))
@@ -552,12 +544,12 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
552544
elseif node.head === :latestworld
553545
frame.world = Base.get_world_counter()
554546
else
555-
rhs = eval_rhs(recurse, frame, node)
547+
rhs = eval_rhs(interp, frame, node)
556548
end
557549
elseif node.head === :thunk || node.head === :toplevel
558550
error("this frame needs to be run at top level")
559551
else
560-
rhs = eval_rhs(recurse, frame, node)
552+
rhs = eval_rhs(interp, frame, node)
561553
end
562554
elseif isa(node, GotoNode)
563555
@assert is_leaf(frame)
@@ -588,7 +580,7 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
588580
rhs = @lookup(frame, node)
589581
end
590582
catch err
591-
return handle_err(recurse, frame, err)
583+
return handle_err(interp, frame, err)
592584
end
593585
@isdefined(rhs) && isa(rhs, BreakpointRef) && return rhs
594586
if isassign(frame, pc)
@@ -603,25 +595,24 @@ function step_expr!(@nospecialize(recurse), frame::Frame, @nospecialize(node), i
603595
end
604596

605597
"""
606-
pc = step_expr!(recurse, frame, istoplevel=false)
598+
pc = step_expr!(interp::Interpreter, frame, istoplevel=false)
607599
pc = step_expr!(frame, istoplevel=false)
608600
609601
Execute the next statement in `frame`. `pc` is the new program counter, or `nothing`
610602
if execution terminates, or a [`BreakpointRef`](@ref) if execution hits a breakpoint.
611603
612-
`recurse` controls call evaluation; `recurse = Compiled()` evaluates :call expressions
613-
by normal dispatch. The default value `recurse = finish_and_return!` will use recursive
604+
`interp` controls call evaluation; `interp = Compiled()` evaluates :call expressions
605+
by normal dispatch. The default value `interp = RecursiveInterpreter()` will use recursive
614606
interpretation.
615607
616608
If you are evaluating `frame` at module scope you should pass `istoplevel=true`.
617609
"""
618-
step_expr!(@nospecialize(recurse), frame::Frame, istoplevel::Bool=false) =
619-
step_expr!(recurse, frame, pc_expr(frame), istoplevel)
620-
step_expr!(frame::Frame, istoplevel::Bool=false) =
621-
step_expr!(finish_and_return!, frame, istoplevel)
610+
step_expr!(interp::Interpreter, frame::Frame, istoplevel::Bool=false) =
611+
step_expr!(interp, frame, pc_expr(frame), istoplevel)
612+
step_expr!(frame::Frame, istoplevel::Bool=false) = step_expr!(RecursiveInterpreter(), frame, istoplevel)
622613

623614
"""
624-
loc = handle_err(recurse, frame, err)
615+
loc = handle_err(interp, frame, err)
625616
626617
Deal with an error `err` that arose while evaluating `frame`. There are one of three
627618
behaviors:
@@ -632,7 +623,7 @@ behaviors:
632623
`loc` is a `BreakpointRef`;
633624
- otherwise, `err` gets rethrown.
634625
"""
635-
function handle_err(@nospecialize(recurse), frame::Frame, @nospecialize(err))
626+
function handle_err(::Interpreter, frame::Frame, @nospecialize(err))
636627
data = frame.framedata
637628
err_will_be_thrown_to_top_level = isempty(data.exception_frames) && !data.caller_will_catch_err
638629
if break_on_throw[] || (break_on_error[] && err_will_be_thrown_to_top_level)
@@ -664,15 +655,16 @@ end
664655
lookup_return(frame, node::ReturnNode) = @lookup(frame, node.val)
665656

666657
"""
667-
ret = get_return(frame)
658+
ret = get_return(interp, frame)
668659
669660
Get the return value of `frame`. Throws an error if `frame.pc` does not point to a `return` expression.
670661
`frame` must have already been executed so that the return value has been computed (see,
671662
e.g., [`JuliaInterpreter.finish!`](@ref)).
672663
"""
673-
function get_return(frame)
664+
function get_return(interp::Interpreter, frame::Frame)
674665
node = pc_expr(frame)
675666
is_return(node) || @invokelatest error("expected return statement, got ", node)
676667
return lookup_return(frame, node)
677668
end
669+
get_return(frame::Frame) = get_return(RecursiveInterpreter(), frame)
678670
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: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,21 @@
1+
abstract type Interpreter end
2+
3+
"""
4+
RecursiveInterpreter <: Interpreter
5+
6+
`RecursiveInterpreter` is a trait that makes JuliaInterpreter routines to recurse into any
7+
`:call` expressions in the code being interpreted. This is the default behavior of JuliaInterpreter.
8+
The default implementations of JuliaInterpreter subroutines are provided by this interpreter.
19
"""
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.
10+
struct RecursiveInterpreter <: Interpreter end
11+
12+
"""
13+
Compiled <: Interpreter
14+
15+
`Compiled` is a trait that makes JuliaInterpreter routines to evaluate any `:call`
16+
expressions in the code being interpreted using Julia's normal compiled-code execution.
517
"""
6-
struct Compiled end
18+
struct Compiled <: Interpreter end
719
Base.similar(::Compiled, sz) = Compiled() # to support similar(stack, 0)
820

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

test/debug.jl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ const ALL_COMMANDS = (:n, :s, :c, :finish, :nc, :se, :si, :until)
77

88
function step_through_command(fr::Frame, cmd::Symbol)
99
while true
10-
ret = JuliaInterpreter.debug_command(JuliaInterpreter.finish_and_return!, fr, cmd)
10+
ret = JuliaInterpreter.debug_command(fr, cmd)
1111
ret == nothing && break
1212
fr, pc = ret
1313
end
@@ -336,7 +336,7 @@ end
336336
try
337337
break_on(:error)
338338
fr = JuliaInterpreter.enter_call(f_outer)
339-
fr, pc = debug_command(JuliaInterpreter.finish_and_return!, fr, :finish)
339+
fr, pc = debug_command(fr, :finish)
340340
@test fr.framecode.scope.name === :error
341341

342342
fundef() = undef_func()

test/interpret.jl

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -361,25 +361,24 @@ f113(;x) = x
361361
frame = JuliaInterpreter.enter_call(f_multi, 1)
362362
nlocals = length(frame.framedata.locals)
363363
@test_throws UndefVarError JuliaInterpreter.lookup_var(frame, JuliaInterpreter.SlotNumber(nlocals))
364-
stack = [frame]
365364
locals = JuliaInterpreter.locals(frame)
366365
@test length(locals) == 2
367366
@test JuliaInterpreter.Variable(1, :x, false) in locals
368-
JuliaInterpreter.step_expr!(stack, frame)
369-
JuliaInterpreter.step_expr!(stack, frame)
367+
JuliaInterpreter.step_expr!(frame)
368+
JuliaInterpreter.step_expr!(frame)
370369
@static if VERSION >= v"1.11-"
371370
locals = JuliaInterpreter.locals(frame)
372371
@test length(locals) == 2
373-
JuliaInterpreter.step_expr!(stack, frame)
372+
JuliaInterpreter.step_expr!(frame)
374373
end
375374
locals = JuliaInterpreter.locals(frame)
376375
@test length(locals) == 3
377376
@test JuliaInterpreter.Variable(1, :c, false) in locals
378-
JuliaInterpreter.step_expr!(stack, frame)
377+
JuliaInterpreter.step_expr!(frame)
379378
locals = JuliaInterpreter.locals(frame)
380379
@test length(locals) == 3
381380
@test JuliaInterpreter.Variable(2, :x, false) in locals
382-
JuliaInterpreter.step_expr!(stack, frame)
381+
JuliaInterpreter.step_expr!(frame)
383382
locals = JuliaInterpreter.locals(frame)
384383
@test length(locals) == 3
385384
@test JuliaInterpreter.Variable(3, :x, false) in locals

0 commit comments

Comments
 (0)