Internals
Basic usage
The process of executing code in the interpreter is to prepare a frame and then evaluate these statements one-by-one, branching via the goto statements as appropriate. Using the summer example described in Lowered representation, let's build a frame:
julia> frame = JuliaInterpreter.enter_call(summer, A)
Frame for summer(A::AbstractArray{T,N} where N) where T in Main at REPL[2]:2
1* 2 1 ─ s = (zero)($(Expr(:static_parameter, 1)))
2 3 │ %2 = A
3 3 │ #temp# = (iterate)(%2)
⋮
A = [1, 2, 5]
T = Int64This is a Frame. Only a portion of the CodeInfo is shown, a small region surrounding the current statement (marked with * or in yellow text). The full CodeInfo can be extracted as code = frame.framecode.src. (It's a slightly modified form of one returned by @code_lowered, in that it has been processed by JuliaInterpreter.optimize! to speed up run-time execution.)
frame has another field, framedata, that holds values needed for or generated by execution. The input arguments and local variables are in locals:
julia> frame.framedata.locals
5-element Vector{Union{Nothing, Some{Any}}}:
Some(summer)
Some([1, 2, 5])
nothing
nothing
nothingThese correspond to the code.slotnames; the first is the #self# argument and the second is the input array. The remaining local variables (e.g., s and a), have not yet been assigned–-we've only built the frame, but we haven't yet begun to execute it. The static parameter, T, is stored in frame.framedata.sparams:
julia> frame.framedata.sparams
1-element Vector{Any}:
Int64The Expr(:static_parameter, 1) statement refers to this value.
The other main storage is for the generated SSA values:
julia> frame.framedata.ssavalues
16-element Vector{Any}:
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undefSince we haven't executed any statements yet, these are all undefined.
The other main entity is the so-called program counter, which just indicates the next statement to be executed:
julia> frame.pc
1Let's try executing the first statement:
julia> JuliaInterpreter.step_expr!(frame)
2This indicates that it ran statement 1 and is prepared to run statement 2. (It's worth noting that the first line included a call to zero, so behind the scenes JuliaInterpreter created a new frame for zero, executed all the statements, and then popped back to frame.) Since the first statement is an assignment of a local variable, let's check the locals again:
julia> frame.framedata.locals
5-element Vector{Union{Nothing, Some{Any}}}:
Some(summer)
Some([1, 2, 5])
Some(0)
nothing
nothingYou can see that the entry corresponding to s has been initialized.
The next statement just retrieves one of the slots (the input argument A) and stores it in an SSA value:
julia> JuliaInterpreter.step_expr!(frame)
3
julia> frame.framedata.ssavalues
16-element Vector{Any}:
#undef
[1, 2, 5]
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undef
#undefOne can easily continue this until execution completes, which is indicated when step_expr! returns nothing. Alternatively, use the higher-level JuliaInterpreter.finish!(frame) to step through the entire frame, or JuliaInterpreter.finish_and_return!(frame) to also obtain the return value.
More complex expressions
Sometimes you might have a whole sequence of expressions you want to run. In such cases, your first thought should be to construct the Frame manually. Here's a demonstration:
julia> using Testjulia> ex = quote x, y = 1, 2 @test x + y == 3 end;julia> frame = Frame(Main, ex);julia> JuliaInterpreter.finish_and_return!(frame)Test Passed
Toplevel code and world age
Code that defines new structs, new methods, or new modules requires special handling. Calling finish_and_return! on a frame that defines and then calls new objects can trigger a world age error, in which a newly defined method is considered too new to be called by currently running code.
Here's a demonstration of the problem:
julia> ex = :(map(x->x^2, [1, 2, 3]));
julia> frame = Frame(Main, ex);
julia> JuliaInterpreter.finish_and_return!(frame)
ERROR: this frame needs to be run at top levelThe reason for this error becomes clearer if we look directly at the lowered code:
julia> Meta.lower(Main, ex):($(Expr(:thunk, CodeInfo( @ none within `top-level scope` 1 ─ $(Expr(:thunk, CodeInfo( @ none within `top-level scope` 1 ─ global var"#3#4" │ const var"#3#4" │ %3 = Core._structtype(Main, Symbol("#3#4"), Core.svec(), Core.svec(), Core.svec(), false, 0) │ Core._setsuper!(%3, Core.Function) │ var"#3#4" = %3 │ Core._typebody!(%3, Core.svec()) └── return nothing ))) │ %2 = Core.svec(var"#3#4", Core.Any) │ %3 = Core.svec() │ %4 = Core.svec(%2, %3, $(QuoteNode(:(#= string:1 =#)))) │ $(Expr(:method, false, :(%4), CodeInfo( @ string:1 within `none` 1 ─ %1 = ^ │ %2 = Core.apply_type(Base.Val, 2) │ %3 = (%2)() │ %4 = Base.literal_pow(%1, x, %3) └── return %4 ))) │ #3 = %new(var"#3#4") │ %7 = #3 │ %8 = Base.vect(1, 2, 3) │ %9 = map(%7, %8) └── return %9 ))))
The code first defines the anonymous function x->x^2 (creating an anonymous type and a call method for it) and then calls map. The problem is that the newly defined call method is in a world newer than the one in which finish_and_return! is running.
This can be fixed by indicating that we want to run this frame at top level:
julia> JuliaInterpreter.finish_and_return!(frame, true)
3-element Vector{Int64}:
1
4
9Interpreting multi-statement programs
When interpreting code with multiple statements — such as the contents of a source file — later statements often call methods or use types defined by earlier ones. Frame handles this automatically for :toplevel expressions (the representation used when parsing multiple statements) and :module expressions:
julia> ex = Meta.parseall(""" f_new(x) = 2x g_new() = f_new(21) g_new() """);julia> ex.head:topleveljulia> frame = Frame(Main, ex);julia> JuliaInterpreter.finish_and_return!(frame)42
Meta.parseall (as opposed to Meta.parse) parses the entire string as a sequence of top-level statements, returning a :toplevel expression. Frame recognizes :toplevel and :module expressions and creates a driver frame that steps through the unlowered surface statements one at a time, raising the world age between each so that each statement sees all definitions from earlier ones.
ExprSplitter remains available for applications that need fine-grained control over when each sub-expression is evaluated; Revise.jl uses it for this purpose.
(Incidentally, JuliaInterpreter.enter_call(map, x->x^2, [1, 2, 3]) works fine on its own, because the anonymous function is defined by the caller — you'll see that the created frame is very simple.)
World-age threading
Each Frame captures the current world age at construction time. For method frames, this world is held fixed throughout execution, so stepping sees a consistent view of the method table even if new methods are defined mid-session. Toplevel driver frames refresh the world before each statement so they always see the latest definitions.