Глава 7. Собственно компилятор

После выполнения своей задачи синтаксического разбора, интерпретатор обладает деревом абстрактного синтаксиса (AST) со всеми операциями, функциями, классами, и пространствами имён кода Python.

Основное задание компилятора состоит в преобразовании имеющегося дерева абстрактного синтаксиса в те инструкции, которые способно понимать ЦПУ:

 

Рисунок 7-1



Эта задача компилятора расщепляется на две составляющие:

  1. Компилятор: Проходит по дереву абстрактного синтаксиса и создаёт CFG (control сow graph), граф управления потоком, который представляет логическую последовательность для исполнения

  2. Ассемблер: Преобразует все узлы CFG в последовательные исполняемые предложения, носящие название байтового кода (bytecode)

Вот визуальное представление процесса компиляции:

 

Рисунок 7-2



[Замечание]Важно

На протяжении данной главы важно помнить, что основной единицей компиляции для CPython выступает модуль. Указываемые в этой главе этапы компиляции и процесс происходят один раз для каждого модуля в вашем проекте.

В этой главе вы сосредоточитесь на компиляции дерева абстрактного синтаксиса модуля в кодовый объект.

PyAST_CompileObject() является основной точкой входа для компилятора CPython. Она берёт дерево абстрактного синтаксиса модуля в качестве своего первичного аргумента, совместно с названием его файла, а также всеми теми глобальными, локальными и PyArena, созданными ранее в процессе интерпретации.

[Замечание]Замечание

Теперь вы начинаете проникать во внутренности компилятора СPython, вместе со стоящими за ними десятками разработок и теорий вычислительной науки. Не откладывайте это в сторону по причине его размера и сложности. После того как вы разобьёте свой компилятор на логические этапы, он станет менее трудным для своего понимания.

Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относятся к компилятору:

Таблица 7-1. Относящиеся к компилятору исходные файлы
Файл Назначение

Python/compile.c

Реалиация компилятора

Include/compile.h

Определение API и типов компилятора

Важные термины

Данная глава опирается на многие термины, которые могут для вас оказаться новыми:

  • Значение состояния компилятора реализуется в виде некого типа контейнера, который содержит одну символическую таблицу.

  • Эта символическая таблица содержит множество имён переменных и необязательно может содержать дочерние символические таблицы

  • Определённый тип компилятора содержит большое число элеменов компилятора.

  • Всякий элемент компилятора содержит множество имён, названий переменных, констант, а также секционных переменных.

  • Элемент компилятора содержит большое число блоков базовых кадров

  • Блоки базовых кадров содержат множество инструкций байтового кода

Контейнер состояния компилятора и его составляющие могут быть проиллюстрированы следующим образом:

 

Рисунок 7-3



Создание экземпляра компилятора

Прежде чем начать, создаётся некое глобальное состояние компилятора. Такая структура состояния компилятора (с типом compiler) содержит применяемые компилятором свойства, такие как флаги компилятора, стек и PyArena. Она также содержит ссылки на иные структуры данных, например, символьную таблицу.

Вот основные поля в состоянии компилятора:

Таблица 7-2. Основные поля состояния компилятора
Поле Тип Назначение

c_arena

PyArena *

Указатель на арену выделения памяти

c_const_cache

PyObject * (dict)

dict Python, удерживающий все константы, в том числе кортежи names

c_do_not_emit_bytecode

int

Флаг для отключения компиляции байтового кода

c_filename

PyObject * (str)

Название подлежащего компиляции файла

c_flags

PyCompilerFlags *

Унаследованные флаги компиляции, см. раздел Флаги компилятора

c_future

int

Указатель на __future__ модуля

c_interactive

int

Флаг для интерактивного режима

c_nestlevel

int

Текущий уровень вложения

c_optimize

PyObject * (dict)

Уровень оптимизации

c_st

symtable *

Символьная таблица компилятора

c_stack

PyObject * (list)

Список Python удерживающий указатели compiler_unit

u

compiler_unit*

Состояние компилятора для текущего блока

Конкретный экземпляр состояния компилятора создаётся внутри PyAST_CompileObject():

  • Если не обладает свойством строки документирования (__doc__), тогда здесь создаётся пустое, как и в случае со свойством __annotations__

  • PyAST_CompileObject() устанавливает все передаваемые значения, такие как название файла состояния компилятора, которое применяется для отслеживания стека и обработки исключительных ситуаций.

  • Арена выделения памяти для компилятора устанавливается в ту, которая используется самим компилятором. За дополнительными сведениями относительно выделения памяти обратитесь к разделу Персональные распределители доменов Главы 9, Управление памятью.

  • Все последующие флаги настраиваются перед компиляцией кода.

Флаги фьючерсов и флаги компилятора

Для переключения функциональных возможностей внутри компилятора существуют два типа флагов, флаги фьючерсов и флаги компилятора. Эти флаги могут быть установлены в двух местах:

  1. В самом состоянии компилятора, которое содержит переменные среды и флаги командной строки

  2. Внутри своего исходного кода модулей посредством использования предложений __future__

Для получения дополнительных сведений относительно состояния конфигурации обратитесь к разделу Состояние конфигурации в Главе 5, Конфигурация и входные данные.

  Флаги фьючерсов

Флаги фьючерсов требуются по причине синтаксиса или функциональных возможностей в данном конкретном модуле. Например, Python 3.7 ввёл отложенную оценку подсказок типа через фьючерсный флаг annotations:


from __future__ import annotations
 	   

Код данного предложения может применять подсказки ещё не разрешённого типа, поэтому требуется данное предложение __future__. В противном случае этот модуль невозможно было бы импортировать.

  Справка по флагам фьючерсов для 3.9

Начиная с версии 3.9 все флаги, за исключением двух (annotations и barry_as_FLUFL) являются обязательными и разрешены автоматически:

Таблица 7-3. Флаги фьючерсов для 3.9
Импорт Назначение

absolute_import

Включает абсолютное импортирование (PEP 328)

annotations

Отложить оценку аннотаций типов (PEP 563)

barry_as_FLUFL

Вложить Пасхальное яйцо (PEP 401)

division

Применять оператор истинного деления (PEP 238)

generator_stop

Разрешить StopIteration внутри генераторов (PEP 479)

generators

Ввести простые генераторы (PEP 255)

nested_scopes

Добавить статически вложенные области действия (PEP 227)

print_function

Превратить print в функцию (PEP 3105)

unicode_literals

Превратить литералы str в Unicode вместо byte (PEP 3112)

annotations

Разрешить предложение with (PEP 343)

[Замечание]Замечание

Основная масса флагов __future__ были применены с целью переносимости между Python 2 и Python 3. Что касается подходов Python 4, вы можете обнаружить ещё большее число добавленных флагов.

  Флаги компилятора

Флаги компилятора специфичны для установленного окружения, поэтому они способны изменять сам ход исполнения кода или тот способ, которым выполняется компилятор, однако они не должны соединяться к определённому источнику, подобно тому как это выполняют предложения __future__.

Одним из примеров флага компиляции мог бы служить -O flag для оптимизации использования предложений assert. Этот флаг отключает все предложения assert, которые могли бы быть помещены в код для целей отладки. Его также можно разрешить при помощи настройки переменной среды PYTHONOPTIMIZE=1.

Символьные таблицы

Прежде чем компилировать код, API PySymtable_BuildObject() создаёт символьную таблицу.

Основная цель такой символьной таблицы состоит в предоставлении списка пространств имён, причём глобальных и локальных, для применения компилятором в ссылках и разрешении областей действия.

  Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относсятся к символьным таблицам:

Таблица 7-4. Относящиеся к компилятору исходные файлы
Файл Назначение

Python/symtable.c

Реализация символьной таблицы

Include/symtable.h

Определение API и типов символьной таблицы

Lib/symtable.py

Модуль стандартной библиотеки symtable

  Структура данных символьной таблицы

Сама структура symtable должна быть одним из экземпляров symtable для компилятора, поэтому существенным становится пространство имён.

К примеру, если вы создаёте метод с названием resolve_names() в одном классе и объявляете другой метод с тем же самым названием в другом классе, тогда вы желали бы знать какой именно метод вы вызываете внутри их модуля.

Соответствующая symtable служит этой цели, а также обеспечению того, что объявленные в узкой области действия переменные не превращаются автоматически в глобальные.

Структура такой символьной таблицы, symtable, обладает следующими полями:

Таблица 7-5. Поля символьной таблицы
Поле Тип Назначение

recursion_depth

int

Текущая глубина рекурсии

recursion_limit

int

Предел рекурсии до возбуждения RecursionError

st_blocks

PyObject * (dict)

Соответствие адресов узлов дерева абстрактного синтаксиса записям символьной таблицы

st_cur

_symtable_entry

Текущая запись символьной таблицы

st_filename

PyObject * (str)

Название подлежащего компиляции файла

st_future

PyFutureFeatures

Фьючерсные функциональные возможности модуля, оказывающие влияние на эту символьную таблицу

st_global

PyObject * (dict)

Ссылки на соответствующие символы из st_top

st_nblocks

int

Значение числа задействованных блоков

st_private

PyObject * (str)

Название текущего класса (необязательно)

st_stack

PyObject * (list)

Стек сведений о пространствах имён

st_top

_symtable_entry

Запись символьной таблицы для данного модуля

  Применение стандартного библиотечного модуля symtable

Некоторые API C символьных таблиц выставляются в Python через модуль symtable из стандартной библиотеки.

При помощи другого модуля с названием tabulate (доступного через PyPI) вы можете создать сценарий для вывода на печать символьной таблицы.

Символьные таблицы могут быть вложенными, поэтому если некий модуль содержит функцию или класс, он также будет обладать и символьной таблицей.

Создайте сценарий с названием symviz.py, содержащий рекурсивную функцию show() , cpython-book-samples/30/symviz.py:


import tabulate
import symtable

code = """
def calc_pow(a, b):
    return a ** b
a = 1
b = 2
c = calc_pow(a,b)
"""

_st = symtable.symtable(code, "example.py", "exec")


def show(table):
    print("Symtable {0} ({1})".format(table.get_name(), 
                                      table.get_type()))
    print(
        tabulate.tabulate(
            [
                (
                    symbol.get_name(),
                    symbol.is_global(),
                    symbol.is_local(),
                    symbol.get_namespaces(),
                )
                for symbol in table.get_symbols()
            ],
            headers=["name", "global", "local", "namespaces"],
            tablefmt="grid",
        )
    )
    if table.has_children():
        [show(child) for child in table.get_children()]


show(_st)
 	   

Запустите symviz.py в своей командной строке чтобы рассмотреть символьную таблицу для примера кода:

 

Рисунок 7-4



  Реализация символьной таблицы

Реализация символьной таблицы пребывает в Python/symtable.c и её первичным интерфейсом выступает PySymtable_BuildObject().

Аналогично рассмотренной в нашей предыдущей главе компиляции дерева абстрактного синтаксиса (AST), PySymtable_BuildObject() выполняет переключения между всеми возможными типами mod_ty (Module, Interactive, Expression и FunctionType) и посещает каждое из предложений в них.

Установленная символьная таблица рекурсивно исследует все узлы и ветви внутри своего дерева абстрактного синтаксиса (с типом mod_ty) и добавляет записи в свою symtable, строка 261 Python/symtable.c:


struct symtable *
PySymtable_BuildObject(mod_ty mod, PyObject *filename,
                       PyFutureFeatures *future)
{
    struct symtable *st = symtable_new();
    asdl_seq *seq;
    int i;
    PyThreadState *tstate;
    int recursion_limit = Py_GetRecursionLimit();
...
    st->st_top = st->st_cur;
    switch (mod->kind) {
    case Module_kind:
        seq = mod->v.Module.body;
        for (i = 0; i < asdl_seq_LEN(seq); i++)
            if (!symtable_visit_stmt(st,
                        (stmt_ty)asdl_seq_GET(seq, i)))
                goto error;
        break;
    case Expression_kind:
        ...
    case Interactive_kind:
        ...
    case FunctionType_kind:
        ...
    }
    ...
}
 	   

Для некоторого модуля PySymtable_BuildObject() обходит в цикле все предложения этого модуля и вызывает symtable_visit_stmt(), которая представляет собой гигантское предложение switch с case для каждого типа (определяемого в Parser/Python.asdl).

Каждый тип оператора обладает соответствующей функцией для разрешения символов. Например, тип предложения определения функции (FunctionDef_kind ) обладает определённой логикой для следующих действий:

  • Проверки текущей глубины рекурсии на предмет превышения установленного предела рекурсии

  • Добавляет соответствующее название функции с свою символьную таблицу с тем, чтобы она была способна вызываться в качестве объекта функции

  • Осуществляет разрешение определённых по умолчанию не литеральных аргументов из своей символьной таблицы

  • Выполняет разрешение типа аннотаций

  • Предпринимает разрешение декораторов функций

Наконец, symtable_enter_block() посещает сам блок с содержимым этой функции. Затем выполняется посещение и разрешение всех аргументов, а также осуществляется обход и разрешение самой функции.

[Замечание]Замечание

Если вы всё ещё удивлены почему изменчивы установленные по умолчанию аргументы Python, основная причина заключается в symtable_visit_stmt(). Установленные по умолчанию аргументы ссылаются на соответствующую переменную из symtable.

Для изменчивости типа никакая дополнительная работа по копированию не производится.

В качестве некого предварительного просмотра вот соответствующий код C для этих этапов построения symtable для некой функции из symtable_visit_stmt(), строка 1171 в Python/symtable.c:


static int
symtable_visit_stmt(struct symtable *st, stmt_ty s)
{
    if (++st->recursion_depth > st->recursion_limit) { 
        PyErr_SetString(PyExc_RecursionError,
           "maximum recursion depth exceeded during compilation");
        VISIT_QUIT(st, 0);
    }
    switch (s->kind) {
    case FunctionDef_kind:
        if (!symtable_add_def(st, s->v.FunctionDef.name, DEF_LOCAL))
            VISIT_QUIT(st, 0);
        if (s->v.FunctionDef.args->defaults)
            VISIT_SEQ(st, expr, s->v.FunctionDef.args->defaults);
        if (s->v.FunctionDef.args->kw_defaults)
            VISIT_SEQ_WITH_NULL(st, expr,
             s->v.FunctionDef.args->kw_defaults);
        if (!symtable_visit_annotations(st, s, s->v.FunctionDef.args,
                                        s->v.FunctionDef.returns))
            VISIT_QUIT(st, 0);
        if (s->v.FunctionDef.decorator_list)
            VISIT_SEQ(st, expr, s->v.FunctionDef.decorator_list);
        if (!symtable_enter_block(st, s->v.FunctionDef.name,
                                  FunctionBlock, (void *)s, s->lineno,
                                  s->col_offset))
            VISIT_QUIT(st, 0);
        VISIT(st, arguments, s->v.FunctionDef.args);
        VISIT_SEQ(st, stmt, s->v.FunctionDef.body);
        if (!symtable_exit_block(st, s))
            VISIT_QUIT(st, 0);
        break;
    case ClassDef_kind: {
        ...
    }
    case Return_kind:
        ...
    case Delete_kind:
        ...
    case Assign_kind:
        ...
    case AnnAssign_kind:
	   

После создания результирующей символьной таблицы, она ппередаётся в компилятор.

Ядро процесса компиляции

Теперь, когда PyAST_CompileObject() обладает состоянием компиляции, symtable и модулем в виде дерева абстрактного синтаксиса (AST), можно приступать к реальной компиляции.

Ядро компилятора имеет две цели:

  1. Преобразовать имеющееся состояние, symtable и дерево абстрактного синтаксиса в control flow graph (CFG) (граф управления потоком)

  2. Защитить свой этап исполнения от исключительных ситуаций времени выполнения путём перехвата всех ошибок логики или кода.

  Доступ к компилятору из Python

Вы можете вызвать свой компилятор в Python через вызов встроеной функции compile(). Она возвращает code object:


>>> co = compile("b+1", "test.py", mode="eval")
>>> co
<code object <module> at 0x10f222780, file "test.py", line 1> 
		

Как и в случае с API symtable(), некое простое выражение должно обладать режимом "eval", а модуль, функция или класс иметь режимом "exec".

Собственно откомпилированный код можно обнаружить в свойстве co_code полученного кодового объекта:


>>> co.co_code
b'e\x00d\x00\x17\x00S\x00'
		

Стандартная библиотека также содержит модуль dis, который выполняет дизассемблирование инструкций байтового кода. Вы можете вывести их на печать на экран или получить список экземпляров Instruction.

[Замечание]Замечание

Тип Instruction в млдуле dis является отражением типа instr из API C.

Когда вы импортируете dis и придадите свойство кодового объекта co_code dis(), тогда эта функция выполнит его дизассемблирование и выведет в REPL на печать его инструкции:


>>> import dis
>>> dis.dis(co.co_code)
          0 LOAD_NAME                0 (0)
          2 LOAD_CONST               0 (0)
          4 BINARY_ADD
          6 RETURN_VALUE
		

LOAD_NAME, LOAD_CONST, BINARY_ADD и RETURN_VALUE, всё это инструкции байтового кода. Они носят название байтового кода (bytecode) по той причине, что все они обладают длиной в один байт. Тем не менее, начиная с Python 3.6, их формат хранения был изменён на word (слово, два байта), поэтому технически более правильно теперь о них говорить как о &qout;коде слова&qout;, а не о байтовом коде.

полный список инструкций байтового кода доступен для каждой версии Python и изменяется от версии к версии. К примеру, в Python 3.7 были введены новые инструкции байтового кода для ускорения особых методов вызова.

В более ранних главах вы познакомились с пакетом instaviz. Он содержит в себе средства визуализации типа кодового объекта, исполняемого компилятором. instaviz также отображает операции байтового кода внутри кодовых объектов.

Снова исполните instaviz, чтобы увидеть объект кода и байтовый код для определённой в REPL функции:


>>> import instaviz
>>> def example():
       a = 1
       b = a + 1
       return b
>>> instaviz.show(example) 
		

  API компилятора C

Точка входа для модуля компиляции дерева абстрактного синтаксиса (AST), compiler_mod(), переключает в различные функции компилятора в зависимости от типа модуля. Если вы предполагаете что mod установлен в Module, тогда в качестве элементов компиляции этот модуль компилируется в свойство c_stack. Затем запускается assemble() для создания PyCodeObject из стека элементов компилятора.

Возвращается полученный кодовый объект и отправляется на исполнение компилятором или сохраняется на диске в виде файла .pyc, в строке 1820 Python|compile.c:


tatic PyCodeObject *
compiler_mod(struct compiler *c, mod_ty mod)
{
    PyCodeObject *co;
    int addNone = 1;
    static PyObject *module;
    ...
    switch (mod->kind) {
    case Module_kind:
        if (!compiler_body(c, mod->v.Module.body)) {
            compiler_exit_scope(c);
            return 0;
        }
        break;
    case Interactive_kind:
        ...
    case Expression_kind:
        ...
    ...
    co = assemble(c, addNone);
    compiler_exit_scope(c);
    return co;
}
 	   

compiler_body() в цикле обходит все предложения из модуля и посещает их, строка 1782 Python/compile.c:


static int
compiler_body(struct compiler *c, asdl_seq *stmts)
{
    int i = 0;
    stmt_ty st;
    PyObject *docstring;
    ...
    for (; i < asdl_seq_LEN(stmts); i++)
        VISIT(c, stmt, (stmt_ty)asdl_seq_GET(stmts, i));
    return 1;
}
 	   

Значение типа предложения определяется посредством вызова asdl_seq_GET(), которое отыскивается по типу узла AST.

Для каждого типа оператора VISIT посредством макро вызывает функцию из Python/compile.c:


#define VISIT(C, TYPE, V) {\
    if (!compiler_visit_ ## TYPE((C), (V))) \
        return 0; \
}
 	   

Для stmt (общего типа предложения, statement), наш компилятор далее вызывает compiler_visit_stmt() и выполняет переключение по всем имеющимся потенциально типам, обнаруживаемым в Parser/Python.asdl, строка 3375 из Python/compile.c:


static int
compiler_visit_stmt(struct compiler *c, stmt_ty s)
{
    Py_ssize_t i, n;

    /* Always assign a lineno to the next instruction for a stmt. */
    SET_LOC(c, s);

    switch (s->kind) {
    case FunctionDef_kind:
        return compiler_function(c, s, 0);
    case ClassDef_kind:
        return compiler_class(c, s);
    ...
    case For_kind:
        return compiler_for(c, s);
    ...
    }

    return 1;
}
 	   

В качестве примера вот представление предложения for в Python:


for i in iterable:
    # block
else:  # optional if iterable is False
    # block
 	   

Вы можете визуализировать это предложение for при помощи железнодорожной схемы:

 

Рисунок 7-5



Когда данное предложение является предложением for, compiler_visit_stmt() вызывает compiler_for(). Для всех имеющихся предложений и выражений имеется некий эквивалент функции compiler_*(). Наиболее непосредственные типы создают идущие походу инструкции байтового кода, хотя некоторые более сложные типы предложений вызывают прочие функции.

  Инструкции

Многие предложения могут обладать подчинёнными предложениями. Цикл for обладает неким телом, но вы также можете иметь более сложные выражения в неком присваивании и итераторе.

Компилятор эмитирует блоки в состояние компиляции. Эти блоки содержат последовательности инструкций. Всякая структура данных инструкции содержит код операции, аргументы, целевой блок (когда это инструкция безусловного перехода, которую вы изучите ниже), а также номер строки её предложения.

  Тип инструкции

Собственно тип инструкции, instr, обладает следующими полями:

Таблица 7-6. Поля типа инструкции
Поле Тип Назначение

i_jabs

unsigned

Флаг, предписывающий что это инструкция абсолютного безусловного перехода

i_jrel

unsigned

Флаг, предписывающий что это инструкция относительного безусловного перехода

i_lineno

int

Номер строки, для которой создана эта инструкция

i_opcode

unsigned char

Номер кода операции, представляемый данной инструкцией (см. Include/Opcode.h)

i_oparg

int

Аргумент кода операции

i_target

basicblock*

Указатель на целевой basicblock, когда выполняется i_jrel

  Инструкции безусловного перехода

Операции безусловного перехода (jump) применяются для безусловного перехода от одной инструкции к другой. Они могут быть либо абсолютными, либо относительными.

Абсолютные инструкции безусловного перехода предписывают точный номер инструкции в компилируемом кодовом объекте, в то время как инструкции относительного безусловного перехода определяют относительное значение цели безусловного перехода для другой инструкции.

  Блоки базового кадра

Блок базового кода (с типом basicblock) содержит следующие поля:

Таблица 7-7. Поля блока базового кода
Поле Тип Назначение

b_ialloc

int

Длина массива инструкций (b_instr)

b_instr

instr *

Указатель на массив инструкций

b_iused

int

Число используемых инструкций (b_instr)

b_list

basicblock *

Список блоков в этом элементе компиляции (в обратном порядке)

b_next

basicblock*

Указатель на следующий блок, достигаемый при обычном управлении потоком

b_offset

int*

Смещение инструкции для данного блока, вычисляемое assemble_jump_offsets()

b_return

unsigned

Истинно, когда вставлен код операции RETURN_VALUE

b_seen

unsigned

Применяется для выполнения глубинного поиска (DFS, см. раздел Сборка)

b_startdepth

int

Глубина стека начиная со входа в этот блок, вычисляемая stackdepth()

  Операции и аргументы

Для различных типов операций требуются различные аргументы. Например, ADDOP_JREL и ADDOP_JABS соответственно ссылаются на "add operation with jump to a relative position" (добавить операцию с безусловным переходом по относительному положению), а "add operation with jump to an absolute position" (добавить операцию с безусловным переходом по абсолютному положению), соответственно.

Существуют и другие макросы: ADDOP_I вызывает compiler_addop_i(), который добавляет операцию с целочисленным агрументом. ADDOP_O вызывает compiler_addop_o(), который добавляет операцию с аргументом PyObject.

Сборка

После того как эти этапы компиляции выполнены, наш компилятор обладает списком кадровых блоков, причём каждый содержит перечень инструкций и некий указатель на следующий блок. Ассемблер выполняет поиск в глубину (DFS, deep- first search) этих кдровых блоков и сливает все инструкции в единую последовательность байтовых кодов.

  Структура данных ассемблера

Структур состояния ассемблера, assembler, определена в Python/compile.c и обладает следующими полями:

Таблица 7-8. Поля структуры состояния ассемблера
Поле Тип Назначение

a_bytecode

PyObject * (str)

Содержащая байтовый код строка

a_lineno

int

Последний lineno эмитированной инструкции

a_lineno_off

int

Смещение байтового кода последней lineno

a_lnotab

PyObject * (str)

Содержащая lnotab строка

a_lnotab_off

int

Смещение в lnotab

a_nblocks

int

Число достигаемых блоков

a_offset

int

Смещение в байтовом коде

a_postorder

basicblock **

Список блоков в обратном порядке глубинного обхода (DFS)

  Алгоритм ассемблера поиска в глубину

Для обхода узлов в базовом графе кадровых блоков ассемблер пользуется глубинным поиском (DFS, deep- first search). Такой алгоритм DFS не является чем- то особенным в CPython и обычно применяется при обходе графов.

В то время как деревья реального и абстрактного синтаксисов (CST и AST), оба, являются древовидными структурами, состояние компилятора представляет собой структуру графа , в которой её узлами выступают базовые кадровые блоки, содержащие инструкции.

Такие базовые кадровые блоки связаны двумя графами. Один представлен в обратном порядке создания на основании свойства b_list каждого блока. Последовательность пронумерованных алфавитно- цифровым образом от A до O базовых кадровых блоков выглядела бы примерно так:

 

Рисунок 7-6



Соответствующий создаваемый по b_list граф применяется для последовательного посещения всех блоков в неком скомпилированном элементе.

Второй граф для каждого из блоков применяет свойство b_next. Данный список представляет собой поток управления. Вершины в этом графе создаются вызовами compiler_use_next_block(c, next), где next это следующий блок для вычерчивания вершины из нашего текущего блока (c->u->u_curblock).

Граф узлов цикла for может выглядеть примерно следующим образом:

 

Рисунок 7-7



Используются и последовательный граф, и граф потока управления, однако в реализации поиска в глубину (DFS) применяется именно граф потока управления.

  API C ассемблера

API ассемблера имеет точку входа, assemble(), которая обладает следующими зонами ответственности:

  • Вычисляет значение числа блоков для выделения памяти.

  • Гарантирует, что всякий поступающий с конца блок возвращает значение none.

  • Выполняет разрешение для всех смещений предложений безусловного перехода, которые помечены как относительные.

  • Для осуществления поиска в глубину в соответствующем блоке вызывает dfs().

  • Эмитирует все имеющиеся инструкции в свой компилятор.

  • Для выработки PyCodeObject вызывает makecode()/

Строка 6021 {Прим. пер.: в тексте 6010} Python/compile.c:


{
    ...
    if (!c->u->u_curblock->b_return) {
        NEXT_BLOCK(c);
        if (addNone)
            ADDOP_LOAD_CONST(c, Py_None);
        ADDOP(c, RETURN_VALUE);
    }
    ...
    dfs(c, entryblock, &a, nblocks);

    /* Can't modify the bytecode after computing jump offsets. */
    assemble_jump_offsets(&a, c);

    /* Emit code in reverse postorder from dfs. */
    for (i = a.a_nblocks - 1; i >= 0; i--) {
        b = a.a_postorder[i];
        for (j = 0; j < b->b_iused; j++)
            if (!assemble_emit(&a, &b->b_instr[j]))
                goto error;
    }
    ...

    co = makecode(c, &a);
 error:
    assemble_free(&a);
    return co;
}
 	   

  Поиск в глубину

Поиск в глубину выполняется dfs() из Python/compile.c, которая следует указателям b_next в каждом из имеющихся блоков, помечает их как просмотренные посредством b_seen, и затем добавляет их в перечень a_postorder ассемблера в обратном порядке.

Эта функция выполняет цикл назад по обратному списку ассемблера и, если она обладает операцией безусловного перехода, рекурсивно вызывает dfs() для такого безусловного перехода, строка 5441 Python/compile.c:


static void
dfs(struct compiler *c, basicblock *b, struct assembler *a, int end)
{
    int i, j;

    /* Get rid of recursion for normal control flow.
       Since the number of blocks is limited, unused space in a_postorder
       (from a_nblocks to end) can be used as a stack for still not ordered
       blocks. */
    for (j = end; b && !b->b_seen; b = b->b_next) {
        b->b_seen = 1;
        assert(a->a_nblocks < j);
        a->a_postorder[--j] = b;
    }
    while (j < end) {
        b = a->a_postorder[j++];
        for (i = 0; i < b->b_iused; i++) {
            struct instr *instr = &b->b_instr[i];
            if (instr->i_jrel || instr->i_jabs)
                dfs(c, instr->i_target, a, j);
        }
        assert(a->a_nblocks < j);
        a->a_postorder[a->a_nblocks++] = b;
    }
}
 	   

После того, как ассемблер создал свой граф в граф управления потоком (CFG) при помощи поиска в глубину, может быть создан кодовый объект.

Создание объекта кода

Задача makecode() состоит в проходе по состоянию компилятора и некоторым свойствам ассемблера и и помещении всего этого в PyCodeObject через вызов PyCode_New().

Значения имён переменных и констант помещаются в качестве свойств этого кодового объекта, строка 5893 Python/compile.c:


static PyCodeObject *
makecode(struct compiler *c, struct assembler *a)
{
...

    consts = consts_dict_keys_inorder(c->u->u_consts);
    names = dict_keys_inorder(c->u->u_names, 0);
    varnames = dict_keys_inorder(c->u->u_varnames, 0);
...
    cellvars = dict_keys_inorder(c->u->u_cellvars, 0);
...
    freevars = dict_keys_inorder(c->u->u_freevars, 
                                 PyTuple_GET_SIZE(cellvars));
...
    flags = compute_code_flags(c);
    if (flags < 0)
        goto error;

    bytecode = PyCode_Optimize(a->a_bytecode, consts, 
                               names, a->a_lnotab);
...
    co = PyCode_NewWithPosOnlyArgs(
        posonlyargcount+posorkeywordargcount,
        posonlyargcount, kwonlyargcount, nlocals_int, 
        maxdepth, flags, bytecode, consts, names,
        varnames, freevars, cellvars, c->c_filename,
        c->u->u_name, c->u->u_firstlineno, a->a_lnotab);
...
    return co;
}
 	   

Вы также можете обратить внимание на то, что этот байтовый код отправляется в PyCode_Optimize() до того как он будет отправлен в PyCode_NewWithPosOnlyArgs(). Эта функция является частью процесса оптимизации байтового кода в Python/peephole.c.

Оптимизатор peephole (смотрового окна) проходит по всем инструкциям байтового кода и, в определённых ситуациях, выполняет в нём замены на иные инструкции. Например, имеется некая оптимизация, которая удаляет все недостижимые инструкции, следующие за предложением return.

Применение Instaviz для отображения объекта кода

При помощи модуля instaviz вы можете собрать воедино все этапы вашего компилятора:


import instaviz

def foo():
    a = 2**4
    b = 1 + 5
    c = [1, 4, 6]
    for i in c:
        print(i)
    else:
        print(a)
    return c


instaviz.show(foo)
 	   

Это воспроизведёт большое и сложное дерево графа AST. Вы можете увидеть свои инструкции байтового кода в последовательности:

 

Рисунок 7-8



Вот объект кода со всеми названиями переменных, констант и исполняемого кода co_code:

 

Рисунок 7-9



Испытайте его с чем- нибудь иным, более сложным кодом, чтобы дополнительно изучить компилятор CPython и объекты кода.

Пример: Реализация оператора почти-равенства

После того как мы рассмотрели компилятор, инструкции получаемого байтового кода, а также ассемблер, вы теперь можете изменить CPython с тем, чтобы он поддерживал оператор почти- равенства, который вы компилировали в соответствующую грамматику в нашей предыдущей главе.

Прежде всего вам придётся добавить внутреннее #define для своего оператора Py_AlE с тем, чтобы на него можно было бы ссылаться внутри всего богатства функций сравнения для PyObject.

Откройте Include/object.h и отыщите следующие предложения #define:


/* Rich comparison opcodes */
#define Py_LT 0
#define Py_LE 1
#define Py_EQ 2
#define Py_NE 3
#define Py_GT 4
#define Py_GE 5
 	   

Добавьте дополнительное значение, Py_AlE, со значением 6:


/* New almost-equal comparator */
#define Py_AlE 6
 	   

Сразу под этим выражением имеется макро, Py_RETURN_RICHCOMPARE. Обновите этот макро с неким предложением выбора для Py_AlE:


/*
 * Macro for implementing rich comparisons
 *
 * Needs to be a macro because any C-comparable type can be used
 */
#define Py_RETURN_RICHCOMPARE(val1, val2, op)                              \
    do {                                                                   \
        switch (op) {                                                      \
        case Py_EQ: if ((val1) == (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
        case Py_NE: if ((val1) != (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
        case Py_LT: if ((val1) < (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE;  \
        case Py_GT: if ((val1) > (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE;  \
        case Py_LE: if ((val1) <= (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
        case Py_GE: if ((val1) >= (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
/* + */ case Py_AlE: if ((val1) == (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE;\
        default:                                                           \
            Py_UNREACHABLE();                                              \
        }                                                                  \
    } while (0)
 	   

Внутри Objects/object.c имеется защита, которая выполняет проверку того что данный оператор находится между 0 и 5. Поскольку вы добавили значение 6, вам придётся обновить и это утверждение, строка 709 /Objects/object.c:


PyObject *
PyObject_RichCompare(PyObject *v, PyObject *w, int op)
{
    PyThreadState *tstate = _PyThreadState_GET();

    assert(Py_LT <= op && op <= Py_GE);
Change that last line to the following:

assert(Py_LT <= op && op <= Py_AlE);
 	   

Измените здесь последнюю строку следующим образом:


assert(Py_LT <= op && op <= Py_AlE);
 	   

Далее вам требуется обновить код операции COMPARE_OP на поддержку Py_AlE в качестве значения для данного типа оператора.

Прежде всего, измените /Objects/object.c и добавьте Py_AlE в список _Py_SwappedOp. Данный перечень применяется для установки соответствия на тот факт, имеет ли данный класс один метод dunder (осадка) и при этом нет иного.

К примеру, когда вы определили некий класс, Coordinate, вы можете определить некий оператор равенства реализовав магический метод __eq__:


class Coordinate:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if isinstance(other, Coordinate):
            return (self.x == other.x and self.y == other.y)
        return super(self, other).__eq__(other)
 	   

Даже хотя вы и не реализовали __ne__ (не равны) для Coordinate, CPython предполагает, что может применяться противоположность __eq__.


>>> Coordinate(1, 100) != Coordinate(2, 400)
True
		

Внутри Objects/object.c отыщите список _Py_SwappedOp и добавьте в его конец Py_AlE. Затем добавьте "~=" в самый конец перечня opstrings:


int _Py_SwappedOp[] = {Py_GT, Py_GE, Py_EQ, Py_NE, Py_LT, Py_LE, Py_AlE};

static const char * const opstrings[]
 = {"<", "<=", "==", "!=", ">", ">=", "~="};
 	   

Откройте Lib/opcode.py и список богатства операторов сравнения:


cmp_op = ("<", "<=", "==", "!=", ">", ">=");
 	   

В конец этого кортежа включите наш новый оператор:


cmp_op = ("<", "<=", "==", "!=", ">", ">=", "~=");
 	   

Список opstrings используется используется для сообщений об ошибках, когда богатство операторов сравнения не реализовано в неком классе.

Затем вы можете обновить свой компилятор на поддержку того случая, когда свойство PyCmp_AlE пребывает в узле BinOp. Откройте Python/compile.c и отыщите compiler_addcompare(), строка 2479 /Python/compile.c:


static int compiler_addcompare(struct compiler *c, cmpop_ty op)
{
    int cmp;
    switch (op) {
    case Eq:
        cmp = Py_EQ;
        break;
    case NotEq:
        cmp = Py_NE;
        break;
    case Lt:
        cmp = Py_LT;
        break;
    case LtE:
        cmp = Py_LE;
        break;
    case Gt:
        cmp = Py_GT;
        break;
    case GtE:
        cmp = Py_GE;
        break;
 	   

Затем добавьте другой case для переключения предложения на пару перечисления comp_op дерева абстрактного синтаксиса AlE с перечислением кода операции сравнения PyCmp_AlE:


..
    case AlE:
        cmp = Py_AlE;
        break;
 	   

Теперь вы способны программировать поведение нашего оператора почти- равенства чтобы он отвечал следующему сценарию:

  • 1 ~= 2 это False.

  • 1 ~= 1.01 это True с применением округления вниз.

Вы можете достичь этого неким дополнительным кодом. Теперь же вы осуществите приведение обоих чисел с плавающей точкой к целым и их сравнением.

API CPython обладает большим числом функций для обработки типов PyLong (int) и PyFloat (float). Это мы рассмотрим в Главе 11, Объекты и типы.

В Objects/floatobject.c отыщите float_richcompare() и под определением Compare: goto добавьте следующий вариант, строка 358 /Objects/floatobject.c:


static PyObject*
float_richcompare(PyObject *v, PyObject *w, int op)
{
 ...
    case Py_GT:
        r = i > j;
        break;
    /* New Code START */
    case Py_AlE: {
        double diff = fabs(i - j);
        double rel_tol = 1e-9; // relative tolerance
        double abs_tol = 0.1;  // absolute tolerance
        r = (((diff <= fabs(rel_tol * j))  ||
              (diff <= fabs(rel_tol * i))) ||
              (diff <= abs_tol));
        }
        break;
    }
    /* New Code END */
    return PyBool_FromLong(r);
 	   

Этот код будет обрабатывать сравнение чисел с плавающей точкой при применении оператора почти- равенства. Он применяет логику, аналогичную math.isclose(), определяемой в PEP 485, но с жёстко запрограммированным допуском в 0.1.

Другим защитником, который вам надлежит изменить является цикл оценки, Python/ceval.c. Вы изучите цикл оценки в нашей следующей главе.

Отыщите такой фрагмент кода:


...
        case TARGET(COMPARE_OP): {
            assert(oparg <= Py_GE);
 	   

Замените эту установку следующим:


assert(oparg <= Py_AlE);
 	   

После повторной компиляции CPythonоткройте REPL и проверьте:


$ ./python
>>> 1.0 ~= 1.01
True
>>> 1.02 ~= 1.01
True
>>> 1.02 ~= 2.01
False
>>> 1 ~= 1.01
True
>>> 1 ~= 1
True
>>> 1 ~= 2
False
>>> 1 ~= 1.9
False
>>> 1 ~= 2.0
False
>>> 1.1 ~= 1.101
True
		

В своей следующей главе мы расширим эту реализацию на прочие типы.

Выводы

В этой главе вы изучили как синтаксически разобранный модуль преобразуется в символьную таблицу, состояние компиляции, а затем и в последовательность операций байтового кода:

 

Рисунок 7-10



Теперь основное задание цикла расчёта ядром интерпретатора CPython для исполнения этих модулей. В своей следующей главе мы исследуем как выполняются объекты кода.