[Перевод] Добавление оператора диапазона в PHP

image
На картинке — Ancient Psychic Tandem War Elephant © Adventure Time

В этой статье будет рассмотрен процесс внедрения в PHP нового оператора. Для этого будут выполнены следующие шаги:

  • Обновление лексического анализатора: он будет знать о синтаксисе нового оператора, что позволит потом превратить его в токен.
  • Обновление парсера: система будет знать, где может использоваться этот оператор, а заодно какова его приоритетность и ассоциативность.
  • Обновление этапа компиляции: здесь происходит обработка (traverse) дерева абстрактного синтаксиса (AST) и извлечение из него кодов операции.
  • Обновление виртуальной машины Zend: во время выполнения скрипта она используется для обработки интерпретации нового кода операции для оператора.


В общем, в этой статье будут кратко рассмотрены несколько внутренних моментов PHP. Выражаю горячую благодарность Никите Попову за помощь в доработке этой статьи.
Так называется оператор, который мы будем добавлять в PHP. Обозначается он двумя символами: |>. Ради простоты его семантика будет определяться следующим образом:

  1. Шаг инкремента всегда будет равен единице.
  2. Операнды могут быть целочисленными или с плавающей запятой.
  3. Если min = max, то будет возвращён одноэлементный массив, содержащий min.


Эти пункты будут разобраны в последнем разделе — «Обновление виртуальной машины Zend», когда мы будем внедрять семантику.

Если хотя бы один пункт не будет выполняться, то будет вылетать исключение Error. То есть:

Если операнд не является целочисленным или с плавающей запятой.
Если min > max.
Если диапазон (max — min) слишком велик.

Примеры:

1 |> 3; // [1, 2, 3]
2.5 |> 5; // [2.5, 3.5, 4.5]

$a = $b = 1;
$a |> $b; // [1]

2 |> 1; // Error exception
1 |> '1'; // Error exception
new StdClass |> 1; // Error exception


Во-первых, нужно зарегистрировать в анализаторе новый токен. Это нужно для того, чтобы при выделении лексем из исходного кода в токен T_RANGE возвращался |>. Для этого придётся обновить файл Zend/zend_language_scanner.l. Добавим в него следующий код (в секцию, где объявляются все токены, примерно 1200-я строка):

"|>" {
    RETURN_TOKEN(T_RANGE);
}


Анализатор сейчас находится в режиме ST_IN_SCRIPTING. Это означает, что он будет определять лишь последовательность символов |>. Между фигурными скобками расположен код на си, который будет выполняться при обнаружении |> в исходном коде. В данном примере происходит возврат токена T_RANGE.

Отступление. Раз уж мы модифицируем лексический анализатор, то для его регенерации нам понадобится Re2c. Для нормальных сборок PHP эта зависимость не нужна.


Идентификатор T_RANGE должен быть объявлен в файле Zend/zend_language_parser.y. Для этого добавим в конец раздела, где объявляются остальные идентификаторы токенов (примерно 220-я строка):

%token T_RANGE           "|> (T_RANGE)"


Теперь PHP распознаёт новый оператор:

1 |> 2; // Parse error: syntax error, unexpected '|>' (T_RANGE) in...


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

Теперь нам нужно регенерировать файл ext/tokenizer/tokenizer_data.c в виде расширения токенизатора, чтобы иметь возможность работать с новым токеном. Это расширение просто предоставляет интерфейс между анализатором и пользовательской средой через функции token_get_all и token_name. В данный момент он находится в счастливом неведении относительно токена T_RANGE:

echo token_name(token_get_all('2;')[2][0]); // UNKNOWN


Для регенерирования ext/tokenizer/tokenizer_data.c идём в папку ext/tokenizer и выполняем файл tokenizer_data_gen.sh. Затем возвращаемся в корневую папку php-src и пересобираем PHP. Проверяем расширение токенизатора:

echo token_name(token_get_all('2;')[2][0]); // T_RANGE


Парсер нужно обновить, чтобы он мог проверять, где в PHP-скриптах используется новый токен T_RANGE. Также парсер отвечает за:

  • определение приоритетности и ассоциативности нового оператора,
  • генерирование нового узла в дереве абстрактного синтаксиса.


Всё это делается с помощью файла грамматики Zend/zend_language_parser.y, содержащего объявления токенов и продукционные правила, которые Bison будет использовать для генерирования парсера.

Отступление. Приоритетность задаёт правила группирования выражений. Например, в выражении 3 + 4×2 символ * имеет более высокий приоритет, чем +, поэтому выражение будет сгруппировано как 3 + (4×2).

Ассоциативность описывает поведение оператора во время выстраивания цепочки: может ли оператор быть встроен в цепочку, и если да, то как он будет сгруппирован внутри конкретного выражения. Допустим, у тернарного оператора левосторонняя ассоциативность, тогда он будет группироваться и исполняться слева направо. То есть выражение

1 ? 0 : 1 ? 0 : 1; // 1

будет исполняться как

(1 ? 0 : 1) ? 0 : 1; // 1

Если исправить это и прописать правостороннюю ассоциативность, то выражение будет исполняться так:

$a = 1 ? 0 : (1 ? 0 : 1); // 0

Есть неассоциативные операторы, которые вообще не могут быть встроены в цепочки. Скажем, оператор >. Так что это выражение будет ошибочным:

1 < $a < 2;


Поскольку оператор диапазона будет осуществлять вычисления в массив, то использовать его в качестве операнда для самого себя будет бессмысленно (например, 1 |> 3 |> 5). Так что сделаем его неассоциативным. А заодно присвоим ему такую же приоритетность, как у комбинированного оператора сравнения (T_SPACESHIP). Это делается с помощью добавления токена T_RANGE в конец следующей строки (примерно 70-я):

%nonassoc T_IS_EQUAL T_IS_NOT_EQUAL T_IS_IDENTICAL T_IS_NOT_IDENTICAL T_SPACESHIP T_RANGE


Теперь для работы с новым оператором нужно обновить правило expr_without_variable. Добавим в него следующий код (например, прямо перед правилом T_SPACESHIP, примерно 930-я строка):

|   expr T_RANGE expr
            { $$ = zend_ast_create(ZEND_AST_RANGE, $1, $3); }


Символ | используется в качестве or. Это означает, что соответствовать может любое правило из перечисленных. При обнаружении соответствия будет выполнен код внутри фигурных скобок. $$ обозначает узел результатов, в котором хранится значение выражения. Функция zend_ast_create применяется для создания нашего AST-узла для нового оператора. Имя узла — ZEND_AST_RANGE, он содержит два значения: $1 ссылается на левый операнд (expr T_RANGE expr), $3 — на правый (expr T_RANGE expr).

Теперь нам нужно задать для AST константу ZEND_AST_RANGE. Для этого обновим файл Zend/zend_ast.h путём простого добавления константы под списком из двух дочерних нод (например, под ZEND_AST_COALESCE):

ZEND_AST_RANGE,


Теперь исполнение нашего оператора диапазона всего лишь подвесит интерпретатор:

1 |> 2;


В результате работы парсера мы получаем дерево AST, которое затем просматривается в обратном порядке. Инициализация исполнения функций осуществляется по мере посещения каждого узла дерева. Инициализируемые функции посылают коды операций, которые позднее при интерпретации исполняются виртуальной машиной Zend.

Компиляция осуществляется в Zend/zend_compile.c. Давайте добавим имя нашего нового AST-узла (ZEND_AST_RANGE) в большой оператор ветвления в функции zend_compile_expr (например, сразу после ZEND_AST_COALESCE, примерно 7200-я строка):

 case ZEND_AST_RANGE:
            zend_compile_range(result, ast);
            return;


Теперь где-нибудь в том же файле нужно объявить функцию zend_compile_range:

void zend_compile_range(znode *result, zend_ast *ast) /* {{{ */
{
    zend_ast *left_ast = ast->child[0];
    zend_ast *right_ast = ast->child[1];
    znode left_node, right_node;

    zend_compile_expr(&left_node, left_ast);
    zend_compile_expr(&right_node, right_ast);

    zend_emit_op_tmp(result, ZEND_RANGE, &left_node, &right_node);
}
/* }}} */


Начнём с разыменования левого и правого операндов узла ZEND_AST_RANGE в переменные-указатели left_ast и right_ast. Далее объявим две переменные znode, в которых будут храниться результат компилирования AST-узлов каждого из двух операндов. Это рекурсивная часть обработки дерева и компиляции его узлов в коды операций.

Теперь с помощью функции zend_emit_op_tmp генерируем код операций ZEND_RANGE с двумя его операндами.

Давайте кратко обсудим коды операций и их типы, чтобы лучше понимать смысл использования функции zend_emit_op_tmp.

Коды операций — это инструкции, которые исполняются виртуальной машиной. Каждый из них имеет:

  • Имя (целочисленная константа).
  • Узел op1 (опционально).
  • Узел op2 (опционально).
  • Узел результатов (опционально). Обычно используется для хранения временного значения операции, которой соответствует данный код.
  • Расширенное значение (extended value) (опционально). Это целочисленное значение, используемое для различения форм поведения для перегруженных (overloaded) опкодов.


Отступление. Опкоды для PHP-скриптов можно выяснить с помощью:

  • PHPDBG: sapi/phpdbg/phpdbg -np* program.php
  • Opcache
  • Расширения Vulcan Logic Disassembler (VLD): sapi/cli/php -dvld.active=1 program.php
  • Если скрипт короткий и простой, то можно воспользоваться 3v4l


Узлы опкодов (структуры znode_op) могут быть разных типов:

  • IS_CV (Compiled Variables). Это простые переменные (вроде $a), кэшируемые в простых массивах для обхода поисков в хэш-таблице. Они появились в PHP 5.1 в качестве оптимизации скомпилированных переменных (Compiled Variables). В VLD они обозначаются с помощью ! n (n — целочисленное).
  • IS_VAR. Для всех сложных выражений, выполняющих роль переменных (вроде $a→b). Могут содержать zval IS_REFERENCE, в VLD обозначаются с помощью $n (n — целочисленное).
  • IS_CONST. Для литеральных значений (например, явно прописанных строковых).
  • IS_TMP_VAR. Временные переменные используются для хранения промежуточного результата выражения (поэтому и существуют недолго). Они могут участвовать в подсчёте ссылок (refcount) (в PHP 7), но не могут содержать zval IS_REFERENCE, потому что временные переменные не могут использоваться в качестве ссылок. В VLD обозначаются с помощью ~n (n — целочисленное).
  • IS_UNUSED. Обычно используется для обозначения op node как неиспользуемого. Но иногда в znode_op.num могут храниться данные для использования виртуальной машиной.


Это возвращает нас обратно к функции zend_emit_op_tmp, которая сгенерирует zend_op типа IS_TMP_VAR. Нам это нужно, потому что наш оператор будет представлять собой выражение, а производимое им значение (массив) будет временной переменной, которая может использоваться в качестве операнда для другого опкода (например, ASSIGN из кода $var = 1 |> 3;).
Для обработки исполнения нашего нового опкода нужно обновить виртуальную машину. Это подразумевает обновление файла Zend/zend_vm_def.h. Добавим в самый конец:

ZEND_VM_HANDLER(182, ZEND_RANGE, CONST|TMP|VAR|CV, CONST|TMP|VAR|CV)
{
    USE_OPLINE
    zend_free_op free_op1, free_op2;
    zval *op1, *op2, *result, tmp;

    SAVE_OPLINE();
    op1 = GET_OP1_ZVAL_PTR_DEREF(BP_VAR_R);
    op2 = GET_OP2_ZVAL_PTR_DEREF(BP_VAR_R);
    result = EX_VAR(opline->result.var);

    // if both operands are integers
    if (Z_TYPE_P(op1) == IS_LONG && Z_TYPE_P(op2) == IS_LONG) {
        // for when min and max are integers
    } else if ( // if both operands are either integers or doubles
        (Z_TYPE_P(op1) == IS_LONG || Z_TYPE_P(op1) == IS_DOUBLE)
        && (Z_TYPE_P(op2) == IS_LONG || Z_TYPE_P(op2) == IS_DOUBLE)
    ) {
        // for when min and max are either integers or floats
    } else {
        // for when min and max are neither integers nor floats
    }

    FREE_OP1();
    FREE_OP2();
    ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();
}


Номер опкода должен быть на единицу больше предыдущего максимального значения, так что можно взять 182. Чтобы быстро узнать последний максимальный номер, загляните в файл Zend/zend_vm_opcodes.h, там в конце есть постоянная ZEND_VM_LAST_OPCODE.

Отступление. Вышеприведённый код содержит несколько псевдомакросов (USE_OPLINE и GET_OP1_ZVAL_PTR_DEREF). Это не настоящие си-макросы, во время генерирования виртуальной машины они заменены скриптом Zend/zend_vm_gen.php, в отличие от процедуры, выполняемой препроцессором в ходе компиляции исходного кода. Так что если вы хотите посмотреть их определения, то обратитесь к файлу Zend/zend_vm_gen.php.


Псевдомакрос ZEND_VM_HANDLER содержит определение каждого опкода. Оно может иметь пять параметров:

  1. Номер опкода (182).
  2. Имя опкода (ZEND_RANGE).
  3. Правильные типы левого операнда (CONST|TMP|VAR|CV) (см. $vm_op_decode в Zend/zend_vm_gen.php).
  4. Правильные типы правого операнда (CONST|TMP|VAR|CV) (см. $vm_op_decode в Zend/zend_vm_gen.php).
  5. Опциональный флаг с расширенным значением для перегруженных кодов (см. $vm_ext_decode в Zend/zend_vm_gen.php).


Учитывая вышеописанное, мы можем увидеть:

// CONST enables for
1 |> 5.0;

// TMP enables for
(2**2) |> (1 + 3);

// VAR enables for
$cmplx->var |> $var[1];

// CV enables for
$a |> $b;


Отступление. Если не используется один или оба операнда, то они помечаются с помощью ANY.


Отступление. TMPVAR появился в ZE 3. Он обрабатывает те же типы узлов опкодов, что и TMP|VAR, но генерирует другой код. TMPVAR генерирует один метод для обработки TMP и VAR, что уменьшает размер виртуальной машины, но требует больше условной логики. А TMP|VAR генерирует отдельные методы для обработки TMP и VAR, что увеличивает размер виртуальной машины, но требует меньше условных конструкций.


Переходим к «телу» нашего определения опкода. Начнём с вызова псевдомакроса USE_OPLINE для объявления переменной opline (структура zend_op). Она будет использоваться для считывания операндов (с помощью псевдомакросов наподобие GET_OP1_ZVAL_PTR_DEREF) и прописывания возвращаемого значения опкода.

Далее объявляем две переменные zend_free_op. Это простые указатели на zval, объявляемые для каждого используемого нами операнда. Они нужны во время проверки, нуждается ли в освобождении какой-то операнд. Затем объявляем четыре переменные zval. op1 и op2 —указатели на эти zval«ы, они содержат значения операнда. Объявляем переменную result для хранения результатов операции опкода. И наконец, объявляем tmp для хранения промежуточного значения операции зацикливания в диапазоне (range looping operation). Это значение будет копироваться в хэш-таблицу при каждой итерации.

Переменные op1 и op2 инициализируются соответственно псевдомакросами GET_OP1_ZVAL_PTR_DEREF и GET_OP2_ZVAL_PTR_DEREF. Также эти макросы отвечают за инициализацию переменных free_op1 и free_op2. Постоянная BP_VAR_R, передаваемая в вышеупомянутые макросы, является флагом типа. Её название расшифровывается как BackPatching Variable Read, она используется при считывании скомпилированных переменных (compiled variables). И в завершение разыменовываем opline и присваиваем result её значение для дальнейшего использования.

Теперь давайте заполним «тело» первого if, при условии, что min и max являются целочисленными:

zend_long min = Z_LVAL_P(op1), max = Z_LVAL_P(op2);
zend_ulong size, i;

if (min > max) {
    zend_throw_error(NULL, "Min should be less than (or equal to) max");
    HANDLE_EXCEPTION();
}

// calculate size (one less than the total size for an inclusive range)
size = max - min;

// the size cannot be greater than or equal to HT_MAX_SIZE
// HT_MAX_SIZE - 1 takes into account the inclusive range size
if (size >= HT_MAX_SIZE - 1) {
    zend_throw_error(NULL, "Range size is too large");
    HANDLE_EXCEPTION();
}

// increment the size to take into account the inclusive range
++size;

// set the zval type to be a long
Z_TYPE_INFO(tmp) = IS_LONG;

// initialise the array to a given size
array_init_size(result, size);
zend_hash_real_init(Z_ARRVAL_P(result), 1);
ZEND_HASH_FILL_PACKED(Z_ARRVAL_P(result)) {
    for (i = 0; i < size; ++i) {
        Z_LVAL(tmp) = min + i;
        ZEND_HASH_FILL_ADD(&tmp);
    }
} ZEND_HASH_FILL_END();
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();


Начнём с определения переменных min и max. Они объявлены как zend_long, которая должна использоваться при объявлении длинных целочисленных (подобно тому, как zend_ulong используется для определения длинных целочисленных без знака). Размер потом объявляется с помощью zend_ulong, содержащей значение размера массива, который будет генерироваться.

Далее выполняется проверка: если min > max, то вылетает исключение Error. Если в качестве первого аргумента в zend_throw_error передать Null, то по умолчанию класс исключений будет Error. С помощью наследования можно точнее настроить данное исключение, создав запись о новом классе в Zend/zend_exceptions.c. Но об этом мы подробнее поговорим в другой раз. Если появляется это исключение, то мы вызываем псевдомакрос HANDLE_EXCEPTION, который переходит к исполнению следующего опкода.

Теперь вычислим размер массива, который нужно будет генерировать. Он должен быть на единицу меньше реального размера, поскольку есть вероятность переполнения в случае, если min = ZEND_LONG_MIN (PHP_INT_MIN) и max = ZEND_LONG_MAX (PHP_INT_MAX).

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

Нам известно, что HT_MAX_SIZE = INT_MAX + 1. Если получившееся значение больше size, то мы можем увеличить последний, не опасаясь переполнения. Это мы и делаем следующим шагом, чтобы величина size соответствовала размеру диапазона.

Теперь меняем тип у zval tmp на IS_LONG. Затем с помощью макроса array_init_size инициализируем result. Этот макрос присваивает result’у тип IS_ARRAY_EX, выделяет память для структуры zend_array (хэш-таблица) и настраивает соответствующую хэш-таблицу. Затем функция zend_hash_real_init выделяет память для структур Bucket, содержащих каждый элемент массива. Второй аргумент — 1 — показывает, что мы хотим сделать её упакованной хэш-таблицей (packed hashtable).

Отступление. Упакованная хэш-таблица — это, по сути, фактический массив (actual array), то есть массив, доступ к которому осуществляется с помощью целочисленных ключей (в отличие от типичных ассоциативных массивов в PHP). Эта оптимизация была осуществлена в PHP 7. Причина данного нововведения заключается в том, что в PHP многие массивы индексируются целыми числами (ключи в порядке возрастания). Упакованные хэш-таблицы обеспечивают прямой доступ к пулу хэш-таблиц. Если вас интересуют подробности новой реализации хэш-таблиц, то обратитесь к статье Никиты.


Отступление. Структура _zend_array имеет два алиаса: zend_array и HashTable.


Заполним массив с помощью макроса ZEND_HASH_FILL_PACKED (определение), который, по сути, отслеживает текущее ведро для последующей вставки. Во время генерирования массива промежуточный результат (элемент массива) хранится в zval«е tmp. Макрос ZEND_HASH_FILL_ADD создаёт копию tmp, вставляет её в текущее ведро хэш-таблицы и переходит к следующему ведру для следующей итерации.

Наконец, макрос ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION (появился в ZE 3 в качестве замены для отдельных вызовов CHECK_EXCEPTION() и ZEND_VM_NEXT_OPCODE(), внедрённых в ZE 2) проверяет, не возникло ли исключение. Оно не возникло, и виртуальная машина переходит к следующему опкоду.

Давайте теперь рассмотрим блок else if:

long double min, max, size, i;

if (Z_TYPE_P(op1) == IS_LONG) {
    min = (long double) Z_LVAL_P(op1);
    max = (long double) Z_DVAL_P(op2);
} else if (Z_TYPE_P(op2) == IS_LONG) {
    min = (long double) Z_DVAL_P(op1);
    max = (long double) Z_LVAL_P(op2);
} else {
    min = (long double) Z_DVAL_P(op1);
    max = (long double) Z_DVAL_P(op2);
}

if (min > max) {
    zend_throw_error(NULL, "Min should be less than (or equal to) max");
    HANDLE_EXCEPTION();
}

size = max - min;

if (size >= HT_MAX_SIZE - 1) {
    zend_throw_error(NULL, "Range size is too large");
    HANDLE_EXCEPTION();
}

// we cast the size to an integer to get rid of the decimal places,
// since we only care about whole number sizes
size = (int) size + 1;

Z_TYPE_INFO(tmp) = IS_DOUBLE;

array_init_size(result, size);
zend_hash_real_init(Z_ARRVAL_P(result), 1);
ZEND_HASH_FILL_PACKED(Z_ARRVAL_P(result)) {
    for (i = 0; i < size; ++i) {
        Z_DVAL(tmp) = min + i;
        ZEND_HASH_FILL_ADD(&tmp);
    }
} ZEND_HASH_FILL_END();
ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION();


Отступление. Мы используем long double в тех случаях, когда возможно одновременное использование целочисленных операндов и с плавающей запятой. Дело в том, что точность double составляет лишь 53 бита, так что при использовании этого типа любое целочисленное больше 253 будет представлено неточно. А у long double точность хотя бы 64 бита, так что он позволяет точно использовать 64-битные целочисленные.


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

  1. к получению их из макроса Z_DVAL_P,
  2. назначению типа IS_DOUBLE для tmp,
  3. а также к вставке zval«а (тип double) с помощью макроса Z_DVAL.


Наконец, нам нужно обрабатывать случаи, в которых либо min, либо max, либо они оба не являются ни целочисленными, ни с плавающей запятой. Как было заявлено во втором пункте семантики нашего оператора диапазона, в качестве операндов поддерживаются только целочисленные и с плавающей запятой. Во всех остальных случаях должно выкидываться исключение Error. Давайте вставим в блок else следующий код:

zend_throw_error(NULL, "Unsupported operand types - only ints and floats are supported");
HANDLE_EXCEPTION();


Теперь мы закончили определять наш опкод, пришло время регенерировать виртуальную машину. Для этого мы запускаем файл Zend/zend_vm_gen.php, а тот воспользуется файлом Zend/zend_vm_def.h для регенерирования Zend/zend_vm_opcodes.h, Zend/zend_vm_opcodes.c и Zend/zend_vm_execute.h.

Пересоберём PHP, чтобы убедиться в работоспособности нашего оператора диапазона:

var_dump(1 |> 1.5);

var_dump(PHP_INT_MIN |> PHP_INT_MIN + 1);


Выходные данные:

array(1) {
  [0]=>
  float(1)
}

array(2) {
  [0]=>
  int(-9223372036854775808)
  [1]=>
  int(-9223372036854775807)
}


Наш оператор наконец-то работает! Но мы ещё не закончили. Осталось обновить pretty printer нашего дерева AST (превращает дерево обратно в код). Pretty printer пока ещё не поддерживает наш новый оператор, убедиться в этом можно с помощью assert():

assert(1 |> 2); // segfaults


Отступление. При сбое assert() использует pretty printer для вывода выражения, которое было заявлено как часть его собственного сообщения об ошибке. Делается это только в том случае, если заявленное выражение представлено не в строковом формате (а иначе pretty printer не нужен). К слову, данная функциональность появилась в PHP 7.


Чтобы это исправить, нам нужно всего лишь обновить файл Zend/zend_ast.c, превратив узел ZEND_AST_RANGE в строку. Для начала изменим комментарий таблицы приоритетов (примерно 520-я строка), назначив нашему новому оператору приоритет 170 (должно совпадать с файлом zend_language_parser.y):

*  170     non-associative == != === !== |>

Затем для обработки ZEND_AST_RANGE вставим в функцию zend_ast_export_ex оператор case (прямо над case ZEND_AST_GREATER):

case ZEND_AST_RANGE:                   BINARY_OP(" |> ",  170, 171, 171);
case ZEND_AST_GREATER:                 BINARY_OP(" > ",   180, 181, 181);
case ZEND_AST_GREATER_EQUAL:           BINARY_OP(" >= ",  180, 181, 181);


Теперь pretty printer обновлён и assert() замечательно работает:

assert(false && 1 |> 2); // Warning: assert(): assert(false && 1 |> 2) failed...


В этой статье затронуто немало вопросов, хотя и довольно поверхностно. Продемонстрированы стадии, через которые проходит движок Zend при запуске PHP-скриптов, как эти стадии связаны друг с другом и как их можно модифицировать ради внедрения в PHP нового оператора. В статье показан лишь один из возможных способов внедрения оператора диапазона. Надеюсь, в будущем я смогу рассказать и о других — возможно, лучших — вариантах, а также подробнее рассмотрю ряд других вопросов (например, создание новых внутренних классов).

© Habrahabr.ru