company_banner

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

Original author: Thomas Punt
  • Translation
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-я строка):

<ST_IN_SCRIPTING>"|>" {
    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('<?php 1|>2;')[2][0]); // UNKNOWN

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

echo token_name(token_get_all('<?php 1|>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/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 нового оператора. В статье показан лишь один из возможных способов внедрения оператора диапазона. Надеюсь, в будущем я смогу рассказать и о других — возможно, лучших — вариантах, а также подробнее рассмотрю ряд других вопросов (например, создание новых внутренних классов).
Mail.ru Group
1190.28
Строим Интернет
Share post

Comments 25

    +6
    А почему всё не как у людей? Есть же во многих языках оператор '...', зачем выдумывать '|>'?
      +1
      Тогда наверное не получилось бы сделать это просто, потому как точка — конкатенация строк
        +2
        Я такой сложности парсеры не писал, но в других языках как-то справляются. Да и глядя, сколько усилий прилагает php к угадыванию типа переменной, не думаю что представляет какую-то проблему отличить три точки от одной.
          +2
          Имею в виду, что стало бы сложнее в рамках этой статьи, ведь здесь цель не добавить этот оператор, а рассмотреть процесс как все происходит внутри
            +2
            Хм… прошу прощения, я подумал речь идёт о реальном добавлении. Для учебных целей — конечно, согласен.

            Хотя синтаксис namespace-ов в php явно делался из соображений «как бы попроще парсить».
              +2
              Когда делались неймспейсы, еще не было AST, и отличить контекст можно было только жуткими хаками, да и то не всегда получалось сделать предсказуемо, вон, с неймспейсами не получилось (а пытались тогда и: и ::).

              Если бы неймспейсы добавлялись сегодня, этой жести с бэкслешом бы, думаю, и не вышло. Хотя уже все привыкли.
        +3
        В php три точки уже используются.
          +2
          в php7 сделали же контекстно зависимый лексер, так что чисто теоритически никаких проблем с тем что бы заюзать три точки для рэйджей нет. Или есть?
            0
            Fesor Да, это можно сделать. Но лично меня все время напрягало, когда язык имеет конструкцию, значение которой зависит от контекста. Видимо мой мозг с контекстом плохо работает :)
              +1
              Ну тут ошибиться сложно будет:

              function some(...$args) {}
              // и
              some(...$args);
              
              // против (включая)
              [1...42]
              // и (исключая)
              [1..42]
              
                0
                some([1...42], ...$args);
                

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

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

                  С другой стороны семантика у операторов всё же похожа — и то, и другое означает некоторую группу, только в первом случае это будет итератор от N до M, а во втором разложение этого итератора на аргументы, т.е. некоторую «запаковку» и «распаковку».

                  Понятно, что нормальный разработчик так писать не будет

                  В реальности можно ещё хуже. :D
                  $args = [6...8];
                  some(...[1...5], ...$args); // some(1, 2, 3, 4, 5, 6, 7, 8);
                  
                    0
                    давайте так:

                    some(...[1..42]);
                    


                    updated: блин, надо дочитывать комментарии
          –9
          В mail.ru теперь пишут на PHP, причём собственной сборки с оператором диапазона? Мне уже страшно…
            +4
            Это перевод, не пугайтесь
              –2
              Все уже давно это поняли! спасибо!
            +2
            В жизни не написал ни единой строчки на PHP, но читать было интересно, спасибо.
              +2
              Если не думать о том, что это перевод и всё сделано фор-фан, а считать, что это реальное будущее техническое требование, то можно было бы заиспользовать вот это решение: github.com/marcioAlmada/yay
                –17
                Жители Виларибо уже написали свой веб-сервис, а жители Вилабаджо все еще допиливают PHP
                  –1
                  Надо же, какие пхпшники обидчивые)) По больному ударил видать… надо было пометить, что это добрая шутка, и сам я тоже иногда пишу на пхп.
                    0
                    Да, к сожалению Хабр такой, иногда 90% шуток приходится обворачивать в теги [joke] или [sarcasm], так как у большинства парсеры их почему-то не проставляют, хотя должны делать это на лету(
                  +5
                  Теперь надо отвечать на stackoverflow примерами со своим оператором.
                    +1
                    Есть еще масса вариантов помимо…
                      +1
                      Картинка просто шикарна. Именно так моя бурная фантазия визуализировала PHP в который добавлены «кастомные операторы» =)
                        0
                        Статья супер!
                        Жду продолжение, где этот оператор будет возвращать генератор, а не генерировать массив! А то ведь -PHP_INT_MAX |> PHP_INT_MAX
                        … =)

                        Only users with full accounts can post comments. Log in, please.