this repo has no description
at trunk 162 lines 6.8 kB view raw view rendered
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.