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 = Int64

This 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
 nothing

These 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}:
 Int64

The 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
 #undef

Since 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
1

Let's try executing the first statement:

julia> JuliaInterpreter.step_expr!(frame)
2

This 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
 nothing

You 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
 #undef

One 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 Test
julia> 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 level

The 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
 9

Interpreting 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:toplevel
julia> 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.