How Python Compiles and Executes The Source Code¶
To understand how Python compiles and executes the source code, I’ll follow along the talk Demystifying Python’s Internals by Sebastiaan Zeeff at PyCon US and note down any useful insight I find. In the talk, Sebastiaan shows a step-by-step guide on how to implement a custom operator in python. My plan is to eventually implement a haskell like Monad operator >>= in Python (see this).
The Big Picture¶
Prerequisite: Setting up devenv¶
Used the base Ubuntu docker image on Linux and installed gcc, python3, make, git and vim.
Cloned cpython git repo.
Switched to branch 3.11 for my experiments for reproducibility (base commit SHA:
4a7612fbecbdd81a6e708e29aab0dc4c6555948d).Compiled cpython source which creates a binary named
pythoninside the build dir.
mkdir build && cd build && ./configure && make -j 8
Adding |> Operator¶
The goal here is to implement a pipe operator, |>, (similar to Linux |) so that we can chain function calls like the following:
def sq(x):
return x**2
def double(x):
return x*2
# usage: functionally the same as sq(sq(double(sq(42)))))
42 |> sq |> double |> sq |> sq
Part 1: From Source Code to Abstract Syntax Tree¶
When Python reads a source code, it first builds an abstract syntax tree (AST). This consists of two parts:
from the source code characters, it creates a sequence of tokens using a
tokenizer.from the tokenized output, it then creates AST using grammar rules that are defined in the parser.
Adding tokenizer support¶
TL;DR¶
Tokens are defined inside the config file
Grammar/Tokens.To Add a new token, we need to specify a
key(e.g.MYTOKENIDENTIFIERKEY) and avalue(=/=) in this file and regerate tokens.The tokens are then automatically given a numeric code (defined in
token.h).Python uses a C functionality (as provided by
tokenizer.c) to tokenize the source code.The same numeric code, key and value are copied inside a python file
token.py.
In-Depth¶
Added a token key VBARGREATER with value |> in this file /cpython/Grammar/Tokens and ran make regen-token to regenerate the tokenizer.
I could see a difference in terms of how a source code is tokenized. To see this, I created a test file with the same python code as above and ran: python -m tokenize test/test.py. Earlier, | and > were identified as separate tokens. Now, each instance of |> is treated as single token.
I could also see that a bunch of other files has also been changed automatically after the token-regeneration.
modified: Doc/library/token-list.inc
modified: Grammar/Tokens
modified: Include/token.h
modified: Lib/token.py
modified: Parser/token.c
Let’s dig deep into see what changes were made in each of these files and try to wrap our heads around what these files are for.
Doc/library/token-list.incThis one is for the Python doc file. It created an entry in the docs for the new token key and value.
Lib/token.pyThis one assigns a numerical code to each of the tokens. Since I added the token in the middle and not at the end, it reassigned the numeric codes for the following tokens as well. Newly added
VBARGREATERgets a code, 54. Number of tokens (N_TOKENS) has increased from 64 to 65. Also, there is a dictionary,EXACT_TOKEN_TYPES, which maintains an inverse map from value to key (e.g.|>to keyVBARGREATER).Include/token.hAdded the same numeric code as above but in the C header.
Parser/token.cThis has an array of token names,
_PyParser_TokenNames, in which it added the new token. In general, this file defines functions that returns numeric codes for tokens of different length (as defined intoken.h), such as,int PyToken_OneChar(int c1),int PyToken_TwoChars(int c1, int c2)andint PyToken_ThreeChars(int c1, int c2, int c3). In the current change, it added a new line of code insidePyToken_TwoCharsin the switch statement to differentiate between|=(already existing token in Python) and the newly added|>. This function is utilised in a giant functionstatic int tok_get(struct tok_state *tok, const char **p_start, const char **p_end)insideParser/tokenizer.c.
