Глава 7. Собственно компилятор
Содержание
- Глава 7. Собственно компилятор
После выполнения своей задачи синтаксического разбора, интерпретатор обладает деревом абстрактного синтаксиса (AST) со всеми операциями, функциями, классами, и пространствами имён кода Python.
Основное задание компилятора состоит в преобразовании имеющегося дерева абстрактного синтаксиса в те инструкции, которые способно понимать ЦПУ:
Эта задача компилятора расщепляется на две составляющие:
-
Компилятор: Проходит по дереву абстрактного синтаксиса и создаёт CFG (control сow graph), граф управления потоком, который представляет логическую последовательность для исполнения
-
Ассемблер: Преобразует все узлы CFG в последовательные исполняемые предложения, носящие название байтового кода (bytecode)
Вот визуальное представление процесса компиляции:
Важно | |
---|---|
На протяжении данной главы важно помнить, что основной единицей компиляции для CPython выступает модуль. Указываемые в этой главе этапы компиляции и процесс происходят один раз для каждого модуля в вашем проекте. |
В этой главе вы сосредоточитесь на компиляции дерева абстрактного синтаксиса модуля в кодовый объект.
PyAST_CompileObject()
является основной точкой входа для компилятора CPython. Она берёт дерево абстрактного синтаксиса модуля в качестве своего первичного аргумента,
совместно с названием его файла, а также всеми теми глобальными, локальными и PyArena
, созданными
ранее в процессе интерпретации.
Замечание | |
---|---|
Теперь вы начинаете проникать во внутренности компилятора СPython, вместе со стоящими за ними десятками разработок и теорий вычислительной науки. Не откладывайте это в сторону по причине его размера и сложности. После того как вы разобьёте свой компилятор на логические этапы, он станет менее трудным для своего понимания. |
Вот те исходные файлы, которые относятся к компилятору:
Файл | Назначение |
---|---|
|
Реалиация компилятора |
|
Определение API и типов компилятора |
Данная глава опирается на многие термины, которые могут для вас оказаться новыми:
-
Значение состояния компилятора реализуется в виде некого типа контейнера, который содержит одну символическую таблицу.
-
Эта символическая таблица содержит множество имён переменных и необязательно может содержать дочерние символические таблицы
-
Определённый тип компилятора содержит большое число элеменов компилятора.
-
Всякий элемент компилятора содержит множество имён, названий переменных, констант, а также секционных переменных.
-
Элемент компилятора содержит большое число блоков базовых кадров
-
Блоки базовых кадров содержат множество инструкций байтового кода
Контейнер состояния компилятора и его составляющие могут быть проиллюстрированы следующим образом:
Прежде чем начать, создаётся некое глобальное состояние компилятора.
Такая структура состояния компилятора (с типом compiler
) содержит применяемые компилятором свойства,
такие как флаги компилятора, стек и PyArena
. Она также содержит ссылки на иные структуры данных,
например, символьную таблицу.
Вот основные поля в состоянии компилятора:
Поле | Тип | Назначение |
---|---|---|
|
|
Указатель на арену выделения памяти |
|
|
|
|
|
Флаг для отключения компиляции байтового кода |
|
|
Название подлежащего компиляции файла |
|
|
Унаследованные флаги компиляции, см. раздел Флаги компилятора |
|
|
Указатель на |
|
|
Флаг для интерактивного режима |
|
|
Текущий уровень вложения |
|
|
Уровень оптимизации |
|
|
Символьная таблица компилятора |
|
|
Список Python удерживающий указатели |
|
|
Состояние компилятора для текущего блока |
Конкретный экземпляр состояния компилятора создаётся внутри PyAST_CompileObject():
-
Если не обладает свойством строки документирования (
__doc__
), тогда здесь создаётся пустое, как и в случае со свойством__annotations__
-
PyAST_CompileObject() устанавливает все передаваемые значения, такие как название файла состояния компилятора, которое применяется для отслеживания стека и обработки исключительных ситуаций.
-
Арена выделения памяти для компилятора устанавливается в ту, которая используется самим компилятором. За дополнительными сведениями относительно выделения памяти обратитесь к разделу Персональные распределители доменов Главы 9, Управление памятью.
-
Все последующие флаги настраиваются перед компиляцией кода.
Для переключения функциональных возможностей внутри компилятора существуют два типа флагов, флаги фьючерсов и флаги компилятора. Эти флаги могут быть установлены в двух местах:
-
В самом состоянии компилятора, которое содержит переменные среды и флаги командной строки
-
Внутри своего исходного кода модулей посредством использования предложений
__future__
Для получения дополнительных сведений относительно состояния конфигурации обратитесь к разделу Состояние конфигурации в Главе 5, Конфигурация и входные данные.
Флаги фьючерсов требуются по причине синтаксиса или функциональных возможностей в данном конкретном модуле. Например, Python 3.7
ввёл отложенную оценку подсказок типа через фьючерсный флаг annotations
:
from __future__ import annotations
Код данного предложения может применять подсказки ещё не разрешённого типа, поэтому требуется данное предложение
__future__
. В противном случае этот модуль невозможно было бы импортировать.
Начиная с версии 3.9 все флаги, за исключением двух (annotations
и
barry_as_FLUFL
) являются обязательными и разрешены автоматически:
Импорт | Назначение |
---|---|
|
Включает абсолютное импортирование (PEP 328) |
|
Отложить оценку аннотаций типов (PEP 563) |
|
Вложить Пасхальное яйцо (PEP 401) |
|
Применять оператор истинного деления (PEP 238) |
|
Разрешить |
|
Ввести простые генераторы (PEP 255) |
|
Добавить статически вложенные области действия (PEP 227) |
|
Превратить |
|
Превратить литералы |
|
Разрешить предложение |
Замечание | |
---|---|
Основная масса флагов |
Флаги компилятора специфичны для установленного окружения, поэтому они способны изменять сам ход исполнения кода или тот способ, которым
выполняется компилятор, однако они не должны соединяться к определённому источнику, подобно тому как это выполняют предложения
__future__
.
Одним из примеров флага компиляции мог бы служить -O flag для оптимизации использования предложений assert
. Этот флаг отключает все
предложения assert
, которые могли бы быть помещены в код для
целей отладки. Его также можно разрешить при помощи
настройки переменной среды PYTHONOPTIMIZE=1
.
Прежде чем компилировать код, API PySymtable_BuildObject() создаёт символьную таблицу.
Основная цель такой символьной таблицы состоит в предоставлении списка пространств имён, причём глобальных и локальных, для применения компилятором в ссылках и разрешении областей действия.
Вот те исходные файлы, которые относсятся к символьным таблицам:
Файл | Назначение |
---|---|
|
Реализация символьной таблицы |
|
Определение API и типов символьной таблицы |
|
Модуль стандартной библиотеки |
Сама структура symtable
должна быть одним из экземпляров symtable
для компилятора, поэтому существенным становится пространство имён.
К примеру, если вы создаёте метод с названием resolve_names()
в одном классе и объявляете другой метод
с тем же самым названием в другом классе, тогда вы желали бы знать какой именно метод вы вызываете внутри их модуля.
Соответствующая symtable
служит этой цели, а также обеспечению того, что объявленные в узкой области
действия переменные не превращаются автоматически в глобальные.
Структура такой символьной таблицы, 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
в своей командной строке чтобы рассмотреть символьную таблицу для примера кода:
Реализация символьной таблицы пребывает в 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().
Установленные по умолчанию аргументы ссылаются на соответствующую переменную из Для изменчивости типа никакая дополнительная работа по копированию не производится. |
В качестве некого предварительного просмотра вот соответствующий код 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), можно
приступать к реальной компиляции.
Ядро компилятора имеет две цели:
-
Преобразовать имеющееся состояние,
symtable
и дерево абстрактного синтаксиса в control flow graph (CFG) (граф управления потоком) -
Защитить свой этап исполнения от исключительных ситуаций времени выполнения путём перехвата всех ошибок логики или кода.
Вы можете вызвать свой компилятор в 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
.
Замечание | |
---|---|
Тип |
Когда вы импортируете 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)
Точка входа для модуля компиляции дерева абстрактного синтаксиса (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;
}
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
при помощи железнодорожной схемы:
Когда данное предложение является предложением for
,
compiler_visit_stmt() вызывает
compiler_for(). Для всех имеющихся
предложений и выражений имеется некий эквивалент функции compiler_*()
. Наиболее непосредственные типы
создают идущие походу инструкции байтового кода, хотя некоторые более сложные типы предложений вызывают прочие функции.
Многие предложения могут обладать подчинёнными предложениями. Цикл for
обладает неким телом,
но вы также можете иметь более сложные выражения в неком присваивании и итераторе.
Компилятор эмитирует блоки в состояние компиляции. Эти блоки содержат последовательности инструкций. Всякая структура данных инструкции содержит код операции, аргументы, целевой блок (когда это инструкция безусловного перехода, которую вы изучите ниже), а также номер строки её предложения.
Собственно тип инструкции, instr
, обладает следующими полями:
Поле | Тип | Назначение |
---|---|---|
|
|
Флаг, предписывающий что это инструкция абсолютного безусловного перехода |
|
|
Флаг, предписывающий что это инструкция относительного безусловного перехода |
|
|
Номер строки, для которой создана эта инструкция |
|
|
Номер кода операции, представляемый данной инструкцией (см.
|
|
|
Аргумент кода операции |
|
|
Указатель на целевой |
Операции безусловного перехода (jump) применяются для безусловного перехода от одной инструкции к другой. Они могут быть либо абсолютными, либо относительными.
Абсолютные инструкции безусловного перехода предписывают точный номер инструкции в компилируемом кодовом объекте, в то время как инструкции относительного безусловного перехода определяют относительное значение цели безусловного перехода для другой инструкции.
Блок базового кода (с типом basicblock
) содержит следующие поля:
Поле | Тип | Назначение |
---|---|---|
|
|
Длина массива инструкций ( |
|
|
Указатель на массив инструкций |
|
|
Число используемых инструкций ( |
|
|
Список блоков в этом элементе компиляции (в обратном порядке) |
|
|
Указатель на следующий блок, достигаемый при обычном управлении потоком |
|
|
Смещение инструкции для данного блока, вычисляемое
|
|
|
Истинно, когда вставлен код операции
|
|
|
Применяется для выполнения глубинного поиска (DFS, см. раздел Сборка) |
|
|
Глубина стека начиная со входа в этот блок, вычисляемая
|
Для различных типов операций требуются различные аргументы. Например, 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
и обладает следующими полями:
Поле | Тип | Назначение |
---|---|---|
|
|
Содержащая байтовый код строка |
|
|
Последний |
|
|
Смещение байтового кода последней |
|
|
Содержащая |
|
|
Смещение в |
|
|
Число достигаемых блоков |
|
|
Смещение в байтовом коде |
|
|
Список блоков в обратном порядке глубинного обхода (DFS) |
Для обхода узлов в базовом графе кадровых блоков ассемблер пользуется глубинным поиском (DFS, deep- first search). Такой алгоритм DFS не является чем- то особенным в CPython и обычно применяется при обходе графов.
В то время как деревья реального и абстрактного синтаксисов (CST и AST), оба, являются древовидными структурами, состояние компилятора представляет собой структуру графа , в которой её узлами выступают базовые кадровые блоки, содержащие инструкции.
Такие базовые кадровые блоки связаны двумя графами. Один представлен в обратном порядке создания на основании свойства
b_list
каждого блока. Последовательность пронумерованных алфавитно- цифровым образом от A до O базовых
кадровых блоков выглядела бы примерно так:
Соответствующий создаваемый по b_list
граф применяется для последовательного посещения всех блоков
в неком скомпилированном элементе.
Второй граф для каждого из блоков применяет свойство b_next
. Данный список представляет собой
поток управления. Вершины в этом графе создаются вызовами compiler_use_next_block(c, next)
, где
next
это следующий блок для вычерчивания вершины из нашего текущего блока
(c->u->u_curblock
).
Граф узлов цикла for
может выглядеть примерно следующим образом:
Используются и последовательный граф, и граф потока управления, однако в реализации поиска в глубину (DFS) применяется именно граф потока управления.
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
вы можете собрать воедино все этапы вашего компилятора:
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. Вы можете увидеть свои инструкции байтового кода в последовательности:
Вот объект кода со всеми названиями переменных, констант и исполняемого кода co_code
:
Испытайте его с чем- нибудь иным, более сложным кодом, чтобы дополнительно изучить компилятор 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
В своей следующей главе мы расширим эту реализацию на прочие типы.
В этой главе вы изучили как синтаксически разобранный модуль преобразуется в символьную таблицу, состояние компиляции, а затем и в последовательность операций байтового кода:
Теперь основное задание цикла расчёта ядром интерпретатора CPython для исполнения этих модулей. В своей следующей главе мы исследуем как выполняются объекты кода.