[Перевод] Добавление оператора диапазона в PHP
На картинке — Ancient Psychic Tandem War Elephant © Adventure Time
В этой статье будет рассмотрен процесс внедрения в PHP нового оператора. Для этого будут выполнены следующие шаги:
- Обновление лексического анализатора: он будет знать о синтаксисе нового оператора, что позволит потом превратить его в токен.
- Обновление парсера: система будет знать, где может использоваться этот оператор, а заодно какова его приоритетность и ассоциативность.
- Обновление этапа компиляции: здесь происходит обработка (traverse) дерева абстрактного синтаксиса (AST) и извлечение из него кодов операции.
- Обновление виртуальной машины Zend: во время выполнения скрипта она используется для обработки интерпретации нового кода операции для оператора.
В общем, в этой статье будут кратко рассмотрены несколько внутренних моментов PHP. Выражаю горячую благодарность Никите Попову за помощь в доработке этой статьи.
Так называется оператор, который мы будем добавлять в PHP. Обозначается он двумя символами: |>. Ради простоты его семантика будет определяться следующим образом:
- Шаг инкремента всегда будет равен единице.
- Операнды могут быть целочисленными или с плавающей запятой.
- Если 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
содержит определение каждого опкода. Оно может иметь пять параметров:
- Номер опкода (182).
- Имя опкода (
ZEND_RANGE
). - Правильные типы левого операнда (CONST|TMP|VAR|CV) (см.
$vm_op_decode
в Zend/zend_vm_gen.php). - Правильные типы правого операнда (CONST|TMP|VAR|CV) (см.
$vm_op_decode
в Zend/zend_vm_gen.php). - Опциональный флаг с расширенным значением для перегруженных кодов (см.
$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-битные целочисленные.
Приведённый выше код по своей логике очень похож на предыдущий. Основное различие заключается в том, что теперь мы обрабатываем данные как числа с плавающей запятой. Это относится:
- к получению их из макроса
Z_DVAL_P
, - назначению типа
IS_DOUBLE для tmp
, - а также к вставке 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 нового оператора. В статье показан лишь один из возможных способов внедрения оператора диапазона. Надеюсь, в будущем я смогу рассказать и о других — возможно, лучших — вариантах, а также подробнее рассмотрю ряд других вопросов (например, создание новых внутренних классов).