Глава 6. Лексический и синтаксический разбор при помощи синтаксических деревьев

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

Этот этап носит название синтаксического разбора (parsing):

 

Рисунок 6-1



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

Для синтаксического анализа в CPython имеются две структуры, а именно, concrete syntax tree (CST, дерево реального синтаксиса) и abstract syntax tree (AST, абстрактное синтаксическое дерево)

 

Рисунок 6-2



Процесс синтаксического разбора обладает двумя частями:

  1. Создание дерева реального синтаксиса при помощи синтаксического анализатора лексем (parser-tokenizer) или лексического анализатора (lexer)

  2. Создание абстрактного синтаксического дерева из реального синтаксического дерева с применением синтаксического разбора (parser)

Эти два этапа являются общей парадигмой, применяемой во многих языках программирования.

Выработка реального синтаксического дерева

Дерево реального синтаксиса (CST, concrete syntax tree), порой именуемое parse tree (деревом синтаксического анализа) является некой упорядоченной, направленной структурой, которая представляет код в контекстно- независимой (контекстно- свободной) грамматике.

CST создаётся из tokenizer (механизма определения лексем - токенов, символов) и parser (синтаксического анализатора). Генератор синтаксического анализатора вы изучили в Главе 4, Язык и грамматика Python. Выводом генератора синтаксического анализа является таблица синтаксического анализа DFA (deterministic finite automaton, конечно- детерменированного автомата), описывающего все возможные состояния контекстно- независимой грамматики.

[Совет]Смотри также

Первоначальный автор Python, Гвидо ван Россум, разработал некую контекстную грамматику для применения в CPython 3.9, в качестве альтернативы LL(1), грамматике, применявшейся в предварительных версиях CPython. Эта новая грамматика носит название PEG (parser expression grammar, грамматика выражений синтаксического анализа).

Такой синтаксический анализатор PEG стал доступным в Python 3.9. В Python 3.10 старая грамматика LL(1) будет удалена окончательно.

В Главе 4, Язык и грамматика Python вы изучили некоторые типы выражений, например, if_stmt и with_stmt. Наше реальное синтаксическое дерево (CST) представляет подобные if_stmt символы, как ветвление с лексемами и терминальными символами в качестве листовых узлов.

К примеру, наше арифместическое выражение a + 1 превращается в такое реальное синтаксическое дерево (CST):

 

Рисунок 6-3



Некое арифметическое выражение представляется здесь с тремя основными ветвями: левой ветвью, ветвью оператора и правой ветвью.

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

Все отражённые в нашем показаном выше реальном синтаксическом дереве символы определены в Grammar/Grammar:


arith_expr: term (('+'|'-') term)*
term: factor (('*'|'@'|'/'|'%'|'//') factor)*
factor: ('+'|'-'|'~') factor | power
power: atom_expr ['**' factor]
atom_expr: [AWAIT] atom trailer*
atom: ('(' [yield_expr|testlist_comp] ')' |
       '[' [testlist_comp] ']' |
       '{' [dictorsetmaker] '}' |
       NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False')
 	   

Все лексемы определены в Grammar/Tokens:


ENDMARKER
NAME
NUMBER
STRING
NEWLINE
INDENT
DEDENT

LPAR                    '('
RPAR                    ')'
LSQB                    '['
RSQB                    ']'
COLON                   ':'
COMMA                   ','
SEMI                    ';'
PLUS                    '+'
MINUS                   '-'
STAR                    '*'
...
 	   

Токен NAME представляет собой название некой переменной, функции, класса или модуля. Синтаксис Python не позволяет чтобы NAME был одним из зарезервированных ключевых слов, как это имеется в случае await и async или численного либо иного иного литерального типа.

Например, если вы попытаетесь определить некую функцию с названием 1, тогда Python возбудит некую SyntaxError:


>>> def 1():
  File "<stdin>", line 1
    def 1():
        ^
SyntaxError: invalid syntax 
		

Токен NAME это определённый тип лексемы для представления одного из множества численных выражений Python. Python обладает особой грамматикой для чисел, включающей следующее:Code

  • Восьмеричные значения, такие как 0o20

  • Шестнадцателичные значения, такие как 0x10

  • Двоичные значения, такие как 0b10000

  • Комплексные числа, такие как 10j

  • Числа с плавающей запятой, такие как 1.01

  • С подчёркиванием в качестве разделителя, такие как 1_000_000

Вы можете увидеть скомпилированные символы и лексемы при помощи модулей symbol и token в Python:


$ ./python
>>> import symbol
>>> dir(symbol)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__',
 '__name__', '__package__', '__spec__', '_main', '_name', '_value',
 'and_expr', 'and_test', 'annassign', 'arglist', 'argument', 
 'arith_expr', 'assert_stmt', 'async_funcdef', 'async_stmt',
 'atom', 'atom_expr',
...
>>> import token
>>> dir(token)
['AMPER', 'AMPEREQUAL', 'AT', 'ATEQUAL', 'CIRCUMFLEX',
 'CIRCUMFLEXEQUAL', 'COLON', 'COMMA', 'COMMENT', 'DEDENT', 'DOT',
 'DOUBLESLASH', 'DOUBLESLASHEQUAL', 'DOUBLESTAR', 'DOUBLESTAREQUAL',
...
 	   

CPython Parser-Tokenizer

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

CPython обладает модулем синтаксического анализа и разбиения на лексемы (parser-tokenizer), написанном на C.

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

Вот связанные с модулем синтаксического анализа и разбиения на лексемы:

Таблица 6-1. Относящиеся к parser-tokenizer исходные файлы
Файл Назначение

Python/pythonrun.c

Исполняет синтаксический анализ и компиляцию из входных данных

Parser/parsetok.c

Реализация синтаксического анализа и разбиения на лексемы

Parser/tokenizer.c

Реализация разбиения на лексемы

Parser/tokenizer.h

Файл заголовков для реализация разбиения на лексемы, который описывает такие модели данных как состояние лексемы

Include/token.h

Определение типов лексем, вырабатываемое Tools/scripts/generate_token.py

Include/node.h

Взаимодействие и макросы узла дерева синтаксического анализа для разбиения на лексемы

  Ввод данных в синтаксический анализатор из файла

Точка входа для синтаксического анализа и разбиения на лексемы, PyParser_ASTFromFileObject(), получает дескриптор файла, флаги компиляции и некий экземпляр PyArena, после чего преобразует соответствующий файловый объект в модуль.

Имеются два этапа:

  1. Преобразование в реальное синтаксическое дерево (CRT) при помощи PyParser_ParseFileObject().

  2. Преобразование в абстрактное синтаксическое дерево (AST) или модуль посредством функции AST PyAST_FromNodeObject().

Функция PyParser_ParseFileObject() обладает двумя важными задачами:

  1. Создание экземпляра состояния разбиения на лексемы, tok_state, при помощи PyTokenizer_FromFile()

  2. Преобразование лексем в реальное синтаксическое дерево (CST, список узлов) посредством parsetok()

  Поток синтаксического анализатора и механизма разбора лексем

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

Перед исполнением сам синтаксический анализатор и механизм разбиения на лексемы устанавливает tok_state, временную структуру данных для хранения всех состояний, вырабатываемых разбиением на лексемы. Конкретное состояние разбиения на лексемы содержит такие сведения, как текущие положения курсора и строки.

Для получения следующей лексемы синтаксический анализатор - механизм разбиения на лексемы вызывает tok_get(). Синтаксический анализатор - механизм разбиения на лексемы передаёт значение идентификатора результирующей лексемы в свой синтаксический анализатор, который использует конечно- детерминированный автомат (DFA) генератора синтаксического анализа для создания некого узла в соответствующем дереве реального синтаксиса.

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

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

 

Рисунок 6-4



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

Значение типа узла определяется в Include/node.h:


typedef struct _node {
    short               n_type;
    char                *n_str;
    int                 n_lineno;
    int                 n_col_offset;
    int                 n_nchildren;
    struct _node        *n_child;
    int                 n_end_lineno;
    int                 n_end_col_offset;
} node;
 	   

Поскольку дерево реального синтаксиса (CST) является деревом синтаксиса, идентификаторами лексем и символами, его компилятору было бы трудно принимать быстрые решения на основе Python.

Прежде чем выполнить безусловный переход в соответствующее дерево абстрактного синтаксиса (AST), существует способ получения вывода с этой стадии синтаксического анализа. CPython обладает стандартным библиотечным модулем, parser, который выставляет необходимые функции C с неким API Python.

Его вывод будет в численном виде, с применением номеров лексем и символов , выработанных на этапе make regen-grammar и хранимых в Include/token.h:


>>> from pprint import pprint
>>> import parser
>>> st = parser.expr('a + 1')
>>> pprint(parser.st2list(st))
[258,
 [332,
  [306,
   [310,
    [311,
     [312,
      [313,
       [316,
        [317,
         [318,
          [319,
           [320,
            [321, [322, [323, [324, [325, [1, 'a']]]]]],
            [14, '+'],
            [321, [322, [323, [324, [325, [2, '1']]]]]]]]]]]]]]]]],
 [4, ''],
 [0, '']] 
		

Чтобы быстрее с этим разобраться, вы можете взять все эти числа из своих модулей symbol и token, поместить их в некий словарь и рекурсивно заменять эти значения в своём выводе parser.st2list() значениями имён соответствующих лексем, cpython-book-samples/21/lex.py:


import symbol
import token
import parser

def lex(expression):
    symbols = {v: k for k, v in symbol.__dict__.items()
               if isinstance(v, int)}
    tokens = {v: k for k, v in token.__dict__.items()
              if isinstance(v, int)}
    lexicon = {**symbols, **tokens}
    st = parser.expr(expression)
    st_list = parser.st2list(st)

    def replace(l: list):
        r = []
        for i in l:
            if isinstance(i, list):
                r.append(replace(i))
            else:
                if i in lexicon:
                    r.append(lexicon[i])
                else:
                    r.append(i)
        return r

    return replace(st_list)
 	   

Чтобы представить как это выглядит в дереве синтаксического анализа, вы можете выполнить lex() с простым выражением, скажем, a + 1:


>>> from pprint import pprint
>>> pprint(lex('a + 1'))

['eval_input',
 ['testlist',
  ['test',
   ['or_test',
    ['and_test',
     ['not_test',
      ['comparison',
       ['expr',
        ['xor_expr',
         ['and_expr',
          ['shift_expr',
           ['arith_expr',
            ['term',
             ['factor', ['power', ['atom_expr', ['atom', 
['NAME', 'a']]]]]],
            ['PLUS', '+'],
            ['term',
             ['factor',
              ['power', ['atom_expr', ['atom', ['NUMBER', 
'1']]]]]]]]]]]]]]]]],
 ['NEWLINE', ''],
 ['ENDMARKER', '']] 
		

В данном выводе вы можете наблюдать имеющиеся символы в нижнем регистре, например, 'arith_expr', а значения лексем в верхнем регистре, такие как 'NUMBER'.

Абстрактные синтаксические деревья

Следующий этап в интерпретаторе CPython состоит в преобразовании выработанное синтаксическим анализатором реальное дерево синтаксиса (CST) в нечто более логичное, что можно исполнить.

Деревья реального синтаксиса через-чур буквальное представление текста из файла кода. На данном этапе это может быть множество языков программирования. Основная грамматическая структура Python прошла интерпретацию, однако вы не могли применять CST для определения функций, областей действий, циклов или каких- либо основных функциональных возможностей языка программирования Python.

Прежде чем компилировать код, реальное дерево синтаксиса (CST) надлежит преобразовать в структуру более высокого уровня, которая представляет реальные конструкции Python. Такая структура является представлением CST, носящим название абстрактного синтаксического дерева (AST, abstract syntax tree).

В качестве примера рассмотрим бинарную операцию в AST с названием BinOp и определяемую как некий тип операции. Она обладает тремя компонентами:

  1. left: Левая часть данной операции

  2. op: сам оператор, например, +, - или *

  3. right: правая часть операции

Соответствующее дерево абстрактного синтаксиса (AST) a + 1 может быть представлено так:

 

Рисунок 6-5



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

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

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

Ниже приводятся исходные файлы, связанные с деревьями абстрактного синтаксиса:

Таблица 6-2. Относящиеся к AST исходные файлы
Файл Назначение

Include/Python-ast.h

Определение типов AST, вырабатываемое Parser/asdl_c.py

Parser/Python.asdl

Список типов и свойств узлов в специфичным для этой области языке, ASDL 5

Python/ast.c

Реализация AST

  Применение Instaviz для представления деревьев абстрактного синтаксиса

Instaviz это пакет Python, написанный для его применения в этой книге. Он отображает AST и скомпилированный код в неком веб- интерфейсе.

Для установки Instaviz, выполните её из pip для пакета instaviz:


$ pip install instaviz
		

Затем откройте REPL, запустив python в своей командной строке без аргументов.

Наша функция instaviz.show() получает отдельный аргумент с типом code object. Вы рассмотрите объект кода в нашей следующей главе. Для данного примера определим некую функцию и воспользуемся названием этой функции в качестве значения своего аргумента:


$ python
>>> import instaviz
>>> def example():
       a = 1
       b = a + 1
       return b

>>> instaviz.show(example) 
		

Вы обнаружите в своей командной строке что был запущен веб сервер с портом 8080. Если вы используете этот порт, вы можете изменить его, вызвав instaviz.show (например, port=9090) или с иным номером порта.

В своём браузере вы сможете увидеть подробный разбор по частям своей функции:

 

Рисунок 6-6



График внизу слева это та функция, которую вы определили в своём REPL, представленное в качестве дерева абстрактного синтаксиса. Каждый узел в этом дереве представлен неким типом AST. Их все можно найти в модуле ast и унаследовать из _ast.AST.

Некоторые из узлов обладают свойствами, которые связывают их с дочерними узлами, в отличии от дерева реального синтаксиса (CST), которое обладало общим свойством дочернего узла.

Например, если вы кликните по своему узлу Assign в центре, тогда он будет связан со строкой b = a + 1:

 

Рисунок 6-7



Этот узел Assign обладает двумя свойствами:

  1. targets это список подлежащих назначению имён. Это именно список, так как вы можете выполнять назначение множеству переменных при помощи единственного выражения, применяя распаковку.

  2. value это подлежащее назначению значение, которое в данном случае является оператором BinOp, а именно a + 1.

Когда вы кликните по оператору BinOp, будут отражены относящиеся к нему свойства:

  1. left находящийся слева от этого оператора узел

  2. op собственно оператор, в нашем случае узел является оператором Add (+) для сложения.

  3. right находящийся справа от этого оператора узел

 

Рисунок 6-8



  Компиляция деревьев абстрактного синтаксиса

Компиляция AST в C является непростой задачей. Соответствующий модуль Python ast.c имеет более 5 000 строк кода.

Существует несколько точек входа, формирующих общедоступную часть API дерева абстрактного синтаксиса (AST). Этот API AST получает некий узел дерева (реального синтаксиса, CST), название файла, значения флагов компиляции и область хранения в памяти.

Получаемым в результате типом является mod_ty, представляющий модуль Python, определяемый в Include Python-ast.h.

mod_ty является структурой контейнера для одного из четырёх типов в Python:

  1. Module

  2. Interactive

  3. Expression

  4. FunctionType

Все эти типы перечислены в Parser/Python.asdl. Вы обнаружите значение типов модулей, типов операторов (предложений), типов выражений, операций (операторов) и понимание того, что все они определены в этом файле.

Значения названий типов в Parser/Python.asdl соотносятся с теми классами, которые выработаны деревом абстрактного типа (AST) и с теми же самыми классами, получившими свои названия в модуле ast стандартной библиотеки:


-- ASDL's 4 builtin types are:
-- identifier, int, string, constant

module Python
{
    mod = Module(stmt* body, type_ignore *type_ignores)
        | Interactive(stmt* body)
        | Expression(expr body)
        | FunctionType(expr* argtypes, expr returns)
 	   

Модуль ast импортирует Include Python-ast.h, файл, автономно создаваемый из Parser/Python.asdl при повторной генерации грамматики. Все параметры и названия в Include Python-ast.h напрямую коррелируют с теми, что определены в Parser/Python.asdl.

Тип mod_ty вырабатывается в Include Python-ast.h из определения Module в Parser/Python.asdl:


enum _mod_kind {Module_kind=1, Interactive_kind=2, Expression_kind=3,
                 FunctionType_kind=4};
struct _mod {
    enum _mod_kind kind;
    union {
        struct {
            asdl_seq *body;
            asdl_seq *type_ignores;
        } Module;

        struct {
            asdl_seq *body;
        } Interactive;

        struct {
            expr_ty body;
        } Expression;

        struct {
            asdl_seq *argtypes;
            expr_ty returns;
        } FunctionType;

    } v;
};
 	   

Соответствующие файл заголовка и структур C применяются для того, чтобы программа Python/ast.c могла быстро выработать необходимые структуры с указателями на относящиеся к делу данные.

Сама точка входа AST, PyAST_FromNodeObject(), по существу, некий оператор switch по результатам от TYPE(n). TYPE() это макро, применяемое AST для определения значения типа узлов в дереве реального синтаксиса. Результатом этого TYPE() будет либо тип некого символа, либо лексемы.

При запуске в корневом узле он может быть только одним из типов модуля, определённого как Module, Interactive, Expression или FunctionType:

  • Для file_input значением типа должен быть Module.

  • Для eval_input, такого как из REPL, значением типа должен быть Expression

Для каждого типа оператора в Python/ast.c существует соответственная функция C ast_for_xxx, которая будет смотреть за узлами реального синтаксического дерева для завершения свойств такого оператора.

Одним из простейших примеров является оператор возведения в степень, например, 2 ** 4, или 2 в 4 степени. ast_for_power() возвратит некую BinOp с оператором Pow (возведение в степень), причём с левой стороны как e (2), а с правой стороны как f (4), строка 2717 Python/ast.c :


static expr_ty
ast_for_power(struct compiling *c, const node *n)
{
    /* power: atom trailer* ('**' factor)*
     */
    expr_ty e;
    REQ(n, power);
    e = ast_for_atom_expr(c, CHILD(n, 0));
    if (!e)
        return NULL;
    if (NCH(n) == 1)
        return e;
    if (TYPE(CHILD(n, NCH(n) - 1)) == factor) {
        expr_ty f = ast_for_expr(c, CHILD(n, NCH(n) - 1));
        if (!f)
            return NULL;
        e = BinOp(e, Pow, f, LINENO(n), n->n_col_offset,
                  n->n_end_lineno, n->n_end_col_offset, c->c_arena);
    }
    return e;
}
 	   

Вы можете увидеть результаты этого, отправив короткую функцию в модуль instaviz:


>>> def foo():
       2**4
>>> import instaviz
>>> instaviz.show(foo)
		
 

Рисунок 6-9



Вы также можете увидеть соответствующие свойства в своём интерфейсе пользователя:

 

Рисунок 6-10



Итак, суммируя, всякий тип оператора и выражения обладают соответствующей функцией ast_for_*() для их создания. Их аргументы определены в Parser/Python.asdl и выставляются через модуль ast в стандартной библиотеке.

Если некое выражение или оператор (предложение) обладают потомками, тогда они вызывают соответствующую дочернюю функцию ast_for_*() при обходе вглубь.

Важные термины для запоминания

Ниже приводятся некоторые ключевые для данной главы термины:

  • Abstract syntax tree (AST, дерево абстрактного синтаксиса): Некое контекстное дерево представления грамматики и синтаксиса Python

  • Concrete syntax tree (CST, дерево реального синтаксиса): Дерево представления лексем и символов без контекста

  • Parse tree (Дерево синтаксического анализа): Другой термин обозначения дерева реального синтаксиса

  • Token: Лексема, некий тип символа, например, +

  • Tokenization (разбор лексем): Процесс преобразования текста в лексемы

  • Parsing (синтаксический разбор): Процесс преобразования текста в CST или AST

Пример: Добавление оператора сравнения почти-равенства

Для сведение всего воедино, вы можете добавить некий фрагмент синтаксиса в язык Python и повторно скомпилировать CPython чтобы разобраться с этим.

Некое выражение сравнения сопоставляет два или более значений:


>>> a = 1
>>> b = 2
>>> a == b
False 
		

Применяемые в выражениях сравнения операторы носят название операторов сравненияю Вот некоторые из тех, с которыми вы знакомы:

  • Меньше чем: <

  • Больше чем: >

  • Равенство: ==

  • Не равенство: !=

[Совет]Смотри также

Для Python 2.1 в PEP 207 в его модели данных были предложены богатые сравнения. Этот PEP содержал контекст, историю и выравнивание для индивидуальных типов Python для реализации методов сравнения.

Теперь давайте добавим другой оператор сравнения с названием почти равны, который будет представляться как ~=. Он будет обладать следующим поведением:

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

  • Когда вы сопоставляете два целых, будут применяться обычные операторы равенства.

В REPL этот новый оператор должен возвращать следующее:


>>> 1 ~= 1
True
>>> 1 ~= 1.0
True
>>> 1 ~= 1.01
True
>>> 1 ~= 1.9
False 
		

Для добавления нового оператора вам вначале следует обновить свою грамматику CPython. В Grammar/python.gram операторы сравнения определяются как символ comp_op:


comparison[expr_ty]:
    | a=bitwise_or b=compare_op_bitwise_or_pair+ ...
    | bitwise_or
compare_op_bitwise_or_pair[CmpopExprPair*]:
    | eq_bitwise_or
    | noteq_bitwise_or
    | lte_bitwise_or
    | lt_bitwise_or
    | gte_bitwise_or
    | gt_bitwise_or
    | notin_bitwise_or
    | in_bitwise_or
    | isnot_bitwise_or
    | is_bitwise_or
eq_bitwise_or[CmpopExprPair*]: '==' a=bitwise_or ...
noteq_bitwise_or[CmpopExprPair*]:
    | (tok='!=' {_PyPegen_check_barry_as_flufl(p) ? NULL : tok}) ...
lte_bitwise_or[CmpopExprPair*]: '<=' a=bitwise_or ...
lt_bitwise_or[CmpopExprPair*]: '<' a=bitwise_or ...
gte_bitwise_or[CmpopExprPair*]: '>=' a=bitwise_or ...
gt_bitwise_or[CmpopExprPair*]: '>' a=bitwise_or ...
notin_bitwise_or[CmpopExprPair*]: 'not' 'in' a=bitwise_or ...
in_bitwise_or[CmpopExprPair*]: 'in' a=bitwise_or ...
isnot_bitwise_or[CmpopExprPair*]: 'is' 'not' a=bitwise_or ...
is_bitwise_or[CmpopExprPair*]: 'is' a=bitwise_or ...
 	   

Изменим выражение compare_op_bitwise_or_pair чтобы разрешить новую пару ale_bitwise_or:


compare_op_bitwise_or_pair[CmpopExprPair*]:
    | eq_bitwise_or
...
    | ale_bitwise_or
 	   

Определим новое выражение ale_bitwise_or под уже имеющимся выражением is_bitwise_or:


...
is_bitwise_or[CmpopExprPair*]: 'is' a=bitwise_or ...
ale_bitwise_or[CmpopExprPair*]: '~=' a=bitwise_or 
    { _PyPegen_cmpop_expr_pair(p, AlE, a) }
 	   

Этот новый тип определяет именованное выражение, ale_bitwise_or, которое содержит терминальный символ '~='.

Этот вызов функции _PyPegen_cmpop_expr_pair(p, AlE, a) является выражением для получения из дерева абстрактного синтаксиса (AST) узла cmpop. Его типом является AlE для Almost Equal.

Затем добавьте в Grammar/Tokens лексему:


ATEQUAL                 '@='
RARROW                  '->'
ELLIPSIS                '...'
COLONEQUAL              ':='
# Add this line
ALMOSTEQUAL             '~='
 	   

Для обновления имеющейся грамматики и лексем в C вам требуется повторно сгенерировать соответствующие заголовки.

В macOS или Linux воспользуйтесь следующей командой:


$ make regen-token regen-pegen
		

В Windows, находясь в каталоге PCBuild воспользуйтесь такой командой:


> build.bat --regen
		

Эти шаги автоматически обновят ваш механизм разбора лексем. Например, откройте исходный код Parser/token.c и вы обнаружите как изменился выбор (case) в функции PyToken_TwoChars():


    case '~':
        switch (c2) {
        case '=': return ALMOSTEQUAL;
        }
        break;
    }
 	   

Когда вы повторно скомпилируете CPython на этом этапе и откроете REPL, тогда вы обнаружите, что ваш механизм распознования лексем успешно распознает эту лексему, однако дерево абстрактного синтаксиса не знает как его обрабатывать:


$ ./python
>>> 1 ~= 2
SystemError: invalid comp_op: ~= 
		

Эта исключительная ситуация возбуждена ast_for_comp_op() внутри Python/ast.c потому как она не распознала ALMOSTEQUAL как допустимый оператор для предложения сравнения.

Compare это тип выражения, определённый в Parser/Python.asdl. Он обладает свойствами для своего левого выражения; список операторов с названием ops, а также список выражений для сравнения с названием comparators:


Compare(expr left, cmpop* ops, expr* comparators)
 	   

Внутри определения Compare имеется справка для перечисления cmpop:


cmpop = Eq | NotEq | Lt | LtE | Gt | GtE | Is | IsNot | In | NotIn
 	   

Это перечень возможных листьевых узлов AST, которые способны действовать как операторы сравнения. Мы пропустили свою и нужно её добавить. Обновите этот список вариантов, чтобы он включал и новую, AlE:


cmpop = Eq | NotEq | Lt | LtE | Gt | GtE | Is | IsNot | In | NotIn | AlE
 	   

Затем снова повторно сгенерируйте дерево абстрактного синтаксиса чтобы обновить файлы заголовков AST:


$ make regen-ast
		

Это обновит перечисление нашего оператора сравнения (_cmpop) внутри Include/Python-ast.h, чтобы он включал и возможность AlE:


typedef enum _cmpop { Eq=1, NotEq=2, Lt=3, LtE=4, Gt=5, GtE=6, Is=7,
                      IsNot=8, In=9, NotIn=10, AlE=11 } cmpop_ty;
 	   

Наше дерево абстрактного синтаксиса не знает что лексема ALMOSTEQUAL эквивалентна оператору сравнения AlE. Поэтому нам необходимо обновить свой код C для AST.

Переместитесь к ast_for_comp_op() из Python/ast.c. Отыщите предложение переключателя для лексем этого оператора. Это вернёт вам одно из перечисляемых значений _cmpop.

Добавьте две строки чтобы отлавливать лексему ALMOSTEQUAL и возвращать оператор сравнения AlE, строка 1222 в Python/ast.c:


static cmpop_ty
ast_for_comp_op(struct compiling *c, const node *n)
{
    /* comp_op: '≪'|'>'|'=='|'>='|'≪='|'!='|'in'|'not' 'in'|'is'
               |'is' 'not'
    */
    REQ(n, comp_op);
    if (NCH(n) == 1) {
        n = CHILD(n, 0);
        switch (TYPE(n)) {
            case LESS:
                return Lt;
            case GREATER:
                return Gt;
            case ALMOSTEQUAL: // Add this line to catch the token
                return AlE;    // And this one to return the AST node
 	   

На этом этапе наши механизм разбора лексем и его дерево абстрактного синтаксиса способны выполнять синтаксический анализ данного кода, однако наш компилятор не может знать как обрабатывать такой оператор. Чтобы проверить представительство нашего AST воспользуйтесь ast.parse() и изучите первый оператор в полученном выражении:


>>> import ast
>>> m = ast.parse('1 ~= 2')
>>> m.body[0].value.ops[0]
<_ast.AlE object at 0x10a8d7ee0> 
		

Это некий экземпляр нашего типа оператора AlE, а потому наше дерево абстрактного синтаксиса правильно провело синтаксический разбор кода.

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

Выводы

Многосторонность и выполнение API нижнего уровня CPythonпревращает его в идеального претендента на встроенный механизм сценариев. Вы обнаружите, что CPython применяется во множестве приложений пользовательского интерфейса, например, в проектировании игр, 3D графике, и автоматизации систем.

Его процесс интерпретации гибкий и действенный. Теперь, когда вы понимаете то, как он работает, вы готовы разобраться с компилятором.