this repo has no description
1# Execution model
2
3Skybison is just the runtime, not a complete Python implementation. CPython
4compiles Python code into *bytecode* that's typically stored in the `.pyc`
5files. Unlike PyPy, we don't have a custom bytecode and we don't plan to have
6one. In fact, we include a fork of CPython's compiler and ask it to compile the
7code for us. Skybison is just trying to execute the bytecode faster while
8preserving the behavior of CPython as closely as possible.
9
10To understand Skybison, it's useful to understand how CPython works internally.
11Check out the resources on the [CPython Developer
12Guide](https://devguide.python.org/exploring/).
13
14> NOTE: This document was written during early project planning stages. Parts
15> of it may be out of date.
16
17## CPython Bytecode
18
19The [compileall module](https://docs.python.org/3/library/compileall.html) can
20be used to compile a file or all files in a directory:
21
22```
23$ python3 -m compileall -b [FILE|DIR]
24```
25
26This will create `.pyc` files in the same directory. If you actually want to
27see the bytecode, use the [dis
28module](https://docs.python.org/3/library/dis.html):
29
30```
31$ python3 -m dis [FILE]
32```
33
34or from inside Python:
35
36```
37>>> import dis
38>>> dis.dis("bar = foo()")
39 1 0 LOAD_NAME 0 (foo)
40 2 CALL_FUNCTION 0
41 4 STORE_NAME 1 (bar)
42 6 LOAD_CONST 0 (None)
43 8 RETURN_VALUE
44```
45
46Complete list of bytecodes with descriptions [can be found
47here](https://docs.python.org/3/library/dis.html#python-bytecode-instructions),
48but this should only be used as an overview, not a specification of what the
49bytecode does. To understand what exactly a bytecode does, read the actual
50CPython implementation in
51[Python/ceval.c](https://github.com/python/cpython/blob/master/Python/ceval.c).
52
53In Skybison, the list of bytecodes can be found in `runtime/bytecode.h` - and
54they are all mapped to interpreter functions implemented in
55`runtime/interpreter.cpp`.
56
57## Interpreter
58
59Skybison has two implementations of its core interpreter loop (opcode dispatch
60and evaluation):
61
62* C++ Interpreter (see `runtime/interpreter.cpp`)
63* Dynamically-Generated x64 Assembly Interpreter (see `runtime/interpreter-gen-x64.cpp`)
64
65The assembly interpreter contains optimized versions of **some** opcodes; the
66rest simply call back into the C++ interpreter. We can also execute the the C++
67interpreter on its own (for non-x64 platforms).
68
69In addition to hand-optimizing the native code generated for each opcode
70handler, the assembly interpreter also allows us to have regularly-sized opcode
71handlers, spaced such that the address of a handler can be computed with a base
72address and the opcode's value. A few special pseudo-handlers are at negative
73offsets from the base address, which are used to handle control flow such as
74exceptions and returning.
75
76The assembly interpreter is dynamically generated at runtime startup; for
77background on this technique you can read the paper "[Generation of Virtual
78Machine Code at
79Startup](https://pdfs.semanticscholar.org/316a/9f4f2e614226b3e613a229a7e3a3f44b1351.pdf)".
80
81There is another important behavioral distinction between the two interpreters
82in how they manage and execute call frames, which is described below.
83
84## Frame Management
85
86An interpreted virtual machine needs to maintain two execution stacks - one
87used by the natively executing interpreter for its own calls, and the one that
88represents the stack of the virtual machine.
89
90CPython accomplishes this by maintaining a virtual Python stack using a linked
91list of `Frame` objects. Since Python provides multiple ways to inspect the
92current state of the interpreter, internal objects like Frame or Thread have to
93be treated as other managed objects - and Python code can retrieve them using
94functions like
95[inspect.currentframe](https://docs.python.org/3/library/inspect.html#inspect.currentframe)
96or
97[threading.current_thread](https://docs.python.org/3/library/threading.html#threading.current_thread).
98However, this is inefficient: frames are inspected rarely but incur consistent
99allocation overhead, and the calling convention does not match the native
100machine's.
101
102Skybison's approach is to allocate an "alternate" stack, created in
103`Thread::Thread()`, which is used as the managed stack, and is laid out similar
104to the machine stack. The assembly interpreter runs directly on this stack (the
105machine's stack pointer registers point into this stack), allowing call /
106return idioms to use native instructions.
107
108The C++ interpreter also knows about the managed stack (and understands the
109frame layout), but it executes on the native stack ([see this Quip for
110details](https://fb.quip.com/VzrCAqXJ7fZn)). When we need to execute C++ code
111(such as a Python function or method which is implemented in C++) we have to
112jump back to executing on the native stack, incurring a performance penalty
113(need to save and restore machine registers).
114
115## Exceptions
116
117Except for bytecode handlers in the interpreter, the procedure for raising an
118exception is to set a pending exception on the current `Thread` and return an
119`Error` object. In the vast majority of cases, this looks something like this:
120
121```cpp
122if (value < 0) {
123 return thread->raiseWithFmt(LayoutId::kValueError, "value cannot be negative");
124}
125```
126
127Consequently, if your code calls a function that could raise an exception, you
128must check for and forward any `Error` return values:
129
130```cpp
131Object result(&scope, someFunction());
132if (result.isError()) return *result;
133```
134
135Note that you should **return** the `Error` object **from the callee**, rather
136than `Error::error()`. Although the various `Error` types are currently
137singletons, we have plans to encode information about the type of the exception
138into the `Error` object at some point. This will allow us to perform parts of
139the unwinding process with only the `Error` object, avoiding loads from the
140current `Thread`.
141
142### In the Interpreter
143
144In the bytecode handler functions (`Interpreter::doLoadFast`, etc.), the first
145step is the same: set the appropriate thread-local state with
146`Thread::raise*()`. Then, instead of returning an `Error` object, return
147`Continue::UNWIND`. This begins the actual unwinding process, including finding
148and jumping to an `except:` handler when appropriate.
149
150### In Tests
151
152To test that your function throws the right exception, use the
153`testing::raisedWithStr()` predicate, like so:
154
155```cpp
156EXPECT_TRUE(raisedWithStr(runBuiltin(FooBuiltins::bar, some_obj),
157 LayoutId::kTypeError, "Argument 1 must be int, str given"));
158```
159
160In addition to being more compact than manually inspecting all the relevant
161thread-local state, using `raisedWithStr()` will make your test future-proof
162for when we encode meaning into the `Error` object.