|
1 | 1 | # Internals
|
2 | 2 |
|
| 3 | +## Basic usage |
| 4 | + |
3 | 5 | The process of executing code in the interpreter is to prepare a `frame` and then
|
4 | 6 | evaluate these statements one-by-one, branching via the `goto` statements as appropriate.
|
5 | 7 | Using the `summer` example described in [Lowered representation](@ref),
|
@@ -150,4 +152,117 @@ julia> frame.ssavalues
|
150 | 152 | ```
|
151 | 153 |
|
152 | 154 | One can easily continue this until execution completes, which is indicated when `step_expr!`
|
153 |
| -returns `nothing`. |
| 155 | +returns `nothing`. Alternatively, use the higher-level `JuliaInterpreter.finish!(stack, frame)` |
| 156 | +to step through the entire frame, |
| 157 | +or `JuliaInterpreter.finish_and_return!(stack, frame)` to also obtain the return value. |
| 158 | +
|
| 159 | +## More complex expressions |
| 160 | +
|
| 161 | +Sometimes you might have a whole sequence of expressions you want to run. |
| 162 | +In such cases, your first thought should be `prepare_thunk`. |
| 163 | +Here's a demonstration: |
| 164 | +
|
| 165 | +```jldoctest; setup=(using JuliaInterpreter; empty!(JuliaInterpreter.junk)) |
| 166 | +using Test |
| 167 | + |
| 168 | +ex = quote |
| 169 | + x, y = 1, 2 |
| 170 | + @test x + y == 3 |
| 171 | +end |
| 172 | + |
| 173 | +frame = JuliaInterpreter.prepare_thunk(Main, ex) |
| 174 | +JuliaInterpreter.finish_and_return!(JuliaStackFrame[], frame) |
| 175 | + |
| 176 | +# output |
| 177 | + |
| 178 | +Test Passed |
| 179 | +``` |
| 180 | +
|
| 181 | +## Toplevel code and world age |
| 182 | +
|
| 183 | +Code that defines new `struct`s, new methods, or new modules is a bit more complicated |
| 184 | +and requires special handling. In such cases, calling `finish_and_return!` on a frame that |
| 185 | +defines these new objects and then calls them can trigger a |
| 186 | +[world age error](https://docs.julialang.org/en/latest/manual/methods/#Redefining-Methods-1), |
| 187 | +in which the method is considered to be too new to be run by the currently compiled code. |
| 188 | +While one can resolve this by using `Base.invokelatest`, we'd have to use that strategy |
| 189 | +throughout the entire package. This would cause a major reduction in performance. |
| 190 | +To resolve this issue without leading to performance problems, care is required to |
| 191 | +return to "top level" after defining such objects. This leads to altered syntax for executing |
| 192 | +such expressions. |
| 193 | +
|
| 194 | +Here's a demonstration of the problem: |
| 195 | +
|
| 196 | +```julia |
| 197 | +ex = :(map(x->x^2, [1, 2, 3])) |
| 198 | +frame = JuliaInterpreter.prepare_thunk(Main, ex) |
| 199 | +julia> JuliaInterpreter.finish_and_return!(JuliaStackFrame[], frame) |
| 200 | +ERROR: this frame needs to be run a top level |
| 201 | +``` |
| 202 | +
|
| 203 | +The reason for this error becomes clearer if we examine `frame` or look directly at the lowered code: |
| 204 | +
|
| 205 | +```julia |
| 206 | +julia> Meta.lower(Main, ex) |
| 207 | +:($(Expr(:thunk, CodeInfo( |
| 208 | +1 ─ $(Expr(:thunk, CodeInfo( |
| 209 | +1 ─ global ##17#18 |
| 210 | +│ const ##17#18 |
| 211 | +│ $(Expr(:struct_type, Symbol("##17#18"), :((Core.svec)()), :((Core.svec)()), :(Core.Function), :((Core.svec)()), false, 0)) |
| 212 | +└── return |
| 213 | +))) |
| 214 | +│ %2 = (Core.svec)(##17#18, Core.Any) |
| 215 | +│ %3 = (Core.svec)() |
| 216 | +│ %4 = (Core.svec)(%2, %3) |
| 217 | +│ $(Expr(:method, false, :(%4), CodeInfo(quote |
| 218 | + (Core.apply_type)(Base.Val, 2) |
| 219 | + (%1)() |
| 220 | + (Base.literal_pow)(^, x, %2) |
| 221 | + return %3 |
| 222 | +end))) |
| 223 | +│ #17 = %new(##17#18) |
| 224 | +│ %7 = #17 |
| 225 | +│ %8 = (Base.vect)(1, 2, 3) |
| 226 | +│ %9 = map(%7, %8) |
| 227 | +└── return %9 |
| 228 | +)))) |
| 229 | +``` |
| 230 | +
|
| 231 | +All of the code before the `%7` line is devoted to defining the anonymous function `x->x^2`: |
| 232 | +it creates a new "anonymous type" (here written as `##17#18`), and then defines a "call |
| 233 | +function" for this type, equivalent to `(##17#18)(x) = x^2`. |
| 234 | +
|
| 235 | +In some cases one can fix this simply by indicating that we want to run this frame at top level: |
| 236 | +
|
| 237 | +```julia |
| 238 | +julia> JuliaInterpreter.finish_and_return!(JuliaStackFrame[], frame, true) |
| 239 | +3-element Array{Int64,1}: |
| 240 | + 1 |
| 241 | + 4 |
| 242 | + 9 |
| 243 | +``` |
| 244 | +
|
| 245 | +Here's a more fine-grained look at what's happening under the hood (and a robust strategy |
| 246 | +for more complex situations where there may be nested calls of new methods): |
| 247 | +
|
| 248 | +```julia |
| 249 | +modexs, _ = JuliaInterpreter.prepare_toplevel(Main, ex) |
| 250 | +stack = JuliaStackFrame[] |
| 251 | +for (mod, e) in modexs |
| 252 | + frame = JuliaInterpreter.prepare_thunk(mod, e) |
| 253 | + while true |
| 254 | + JuliaInterpreter.through_methoddef_or_done!(stack, frame) === nothing && break |
| 255 | + end |
| 256 | + JuliaInterpreter.get_return(frame) |
| 257 | +end |
| 258 | +``` |
| 259 | +
|
| 260 | +This splits the expression into a sequence of frames (here just one, but more complex blocks may be split up into many). |
| 261 | +Then, each frame is executed until it finishes defining a new method, then returns to top level. |
| 262 | +The return to top level causes an update in the world age. |
| 263 | +If the frame hasn't been finished yet (if the return value wasn't `nothing`), |
| 264 | +this continues executing where it left off. |
| 265 | +
|
| 266 | +(Incidentally, `JuliaInterpreter.enter_call(map, x->x^2, [1, 2, 3])` works fine on its own, |
| 267 | +because the anonymous function is defined by the caller---you'll see that the created frame |
| 268 | +is very simple.) |
0 commit comments