Статьи

Реализация оператора диапазона в PHP

Иногда мы сталкиваемся с некоторыми удивительными публикациями в других местах , и с разрешения их авторов публикуем их в SitePoint. Это один из таких примеров. В посте ниже Если вы когда-либо интересовались внутренними компонентами PHP и добавляли функции в ваш любимый язык программирования, сейчас самое время учиться!

В этой статье предполагается, что читатель может построить PHP из исходного кода. Если это не так, пожалуйста, сначала ознакомьтесь с главой « Сборка PHP» в книге PHP Internals .

Летающий слон


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

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

Поэтому эта статья призвана дать краткий обзор ряда внутренних аспектов PHP.

Также большое спасибо Никите Попову за корректуру и помощь в улучшении моей статьи!

Оператор диапазона

Оператор, который будет добавлен в PHP в этой статье, будет называться оператором диапазона ( |> ). Для простоты оператор диапазона будет определен со следующей семантикой:

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

(Все вышеперечисленные пункты будут упоминаться в последнем разделе, Обновление Zend VM , когда мы наконец реализуем вышеупомянутую семантику.)

Если какая-либо из вышеупомянутых семантик не удовлетворена, то будет Error исключение Error . Поэтому это произойдет, если:

  • либо операнд не является целым числом или плавающей точкой
  • мин> макс
  • диапазон (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 — это состояние, в котором в данный момент находится ST_IN_SCRIPTING . Это означает, что он будет соответствовать последовательности символов |> только в обычном режиме сценариев. Код между фигурными скобками — это C-код, который будет выполняться, когда в исходном коде будет найден |> . В этом примере он просто возвращает токен 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 в расширении tokenizer, чтобы обслуживать только что добавленный токен. (Расширение токенизатора просто предоставляет интерфейс для лексера PHP для пользовательского token_get_all через функции 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 

Обновление парсера

Теперь необходимо обновить синтаксический анализатор, чтобы он мог проверить, где новый токен T_RANGE используется в сценариях PHP. Он также отвечает за указание приоритета и ассоциативности нового оператора и генерацию узла абстрактного синтаксического дерева (AST) для новой конструкции. Все это будет сделано в файле грамматики 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 

Некоторые операторы, однако, неассоциативны и поэтому не могут быть связаны вообще. Например, оператор less than ( > ) выглядит следующим образом, поэтому следующее недопустимо:

 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 ) ; } 

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

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

 ZEND_AST_RANGE , 

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

 1 | > 2 ; 

Пришло время обновить этап компиляции.

Обновление этапа компиляции

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

Эта компиляция происходит в Zend / zend_compile.c , поэтому давайте начнем с добавления нашего нового имени узла AST ( ZEND_AST_RANGE ) в большой оператор switch в функции 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 для обоих операндов (это рекурсивная часть обхода AST и компиляции его узлов в коды операций).

Затем мы ZEND_RANGE код операции ZEND_RANGE с двумя его операндами, используя функцию zend_emit_op_tmp .

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

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

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

Отступление:

Коды операций для сценария PHP можно увидеть с помощью:


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

  • IS_CV — для скомпилированных V ariables. Это простые переменные (например, $a ), которые кэшируются в простом массиве для обхода поиска в хеш-таблицах. Они были введены в PHP 5.1 под оптимизацией скомпилированных переменных. Они обозначены !n в VLD (где n — целое число)
  • IS_VAR — для всех других не IS_VAR на переменные выражений, таких как $a->b . Они могут содержать IS_REFERENCE zval и обозначены $n в VLD (где n — целое число)
  • IS_CONST — для литеральных значений (например, жестко закодированных строк)
  • IS_TMP_VAR — временные переменные используются для хранения промежуточного результата выражения (что делает их обычно недолговечными). Они также могут быть пересчитаны (начиная с PHP 7), но не могут содержать IS_REFERENCE zval (потому что временные значения не могут использоваться в качестве ссылок). Они обозначены через ~n в VLD (где n — целое число)
  • IS_UNUSED — обычно используется для пометки операционного узла как неиспользуемого. Однако иногда данные будут храниться в znode_op.num который будет использоваться в виртуальной znode_op.num .

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

Обновление Zend VM

Теперь нам нужно обновить виртуальную машину 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 ). Это не настоящие макросы C — вместо этого они заменяются скриптом Zend / zend_vm_gen.php во время генерации виртуальной машины, а не препроцессором во время компиляции исходного кода. Поэтому, если вы хотите посмотреть их определения, вам нужно покопаться в файле Zend / zend_vm_gen.php .


ZEND_VM_HANDLER содержит определение каждого кода операции. Может иметь 5 параметров:

  1. Номер кода операции (182)
  2. Название кода операции (ZEND_RANGE)
  3. Допустимые типы левых операндов (CONST | TMP | VAR | CV) (см. $vm_op_decode в Zend / zend_vm_gen.php для всех типов)
  4. Допустимые типы правых операндов (CONST | TMP | VAR | CV) (то же самое)
  5. Необязательный флаг, содержащий расширенное значение для перегруженных $vm_ext_decode операций (см. $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 был введен в ZE3 и похож на 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 s, которые содержат значения операндов. объявлен result для сохранения результата операции кода операции. Наконец, tmp используется в качестве промежуточного значения операции циклического диапазона, которая будет копироваться после каждой итерации в хеш-таблицу.

Затем переменные op1 и op2 инициализируются псевдо-макросами GET_OP1_ZVAL_PTR_DEREF и GET_OP2_ZVAL_PTR_DEREF соответственно. Эти псевдо-макросы также отвечают за инициализацию переменных free_op1 и free_op2 . Константа BP_VAR_R которая передается в вышеупомянутые макросы, является флагом типа. Он расшифровывается как BackPatching Variable Read и используется при извлечении скомпилированных переменных . Наконец, результат 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 исключение Error . Передав NULL в качестве первого аргумента zend_throw_error , класс исключения по умолчанию принимает значение Error . Мы могли бы специализировать это исключение, подклассифицировав Error и сделав новую запись класса в Zend / zend_exceptions.c , но это, вероятно, лучше всего будет рассмотрено в следующей статье. Если HANDLE_EXCEPTION исключение, то мы HANDLE_EXCEPTION псевдомакрос 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 теперь соответствовал диапазону.

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


Отступление:

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


Примечание. Структура _zend_array имеет два псевдонима: zend_array и zend_array .

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

Наконец, макрос ZEND_VM_NEXT_OPCODE_CHECK_EXCEPTION (введенный в ZE3 для замены отдельных вызовов CHECK_EXCEPTION() и ZEND_VM_NEXT_OPCODE() выполненных в ZE2), проверяет, произошло ли исключение. Если исключение не произошло, виртуальная машина переходит к следующему коду операции.

Давайте теперь посмотрим на блок 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 бита, и поэтому любое целое число, большее 2 ^ 53, не может быть точно представлено как double . long double , с другой стороны, имеет точность не менее 64 битов, поэтому он может точно представлять 64-битные целые числа.

Этот код очень похож на предыдущую логику. Основное отличие теперь состоит в том, что мы обрабатываем данные как числа с плавающей запятой. Это включает их Z_DVAL_P с Z_DVAL_P макроса Z_DVAL_P , установку информации о типе для tmp на IS_DOUBLE и вставку zval (типа double) с Z_DVAL макроса Z_DVAL .

Наконец, мы должны обработать случай, когда либо (или оба) min и max не являются целыми числами или числами с плавающей точкой. Как определено в пункте # 2 нашей семантики операторов диапазона, в качестве операндов поддерживаются только целые числа и числа с плавающей запятой — если что-либо еще предусмотрено, будет выдано исключение 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 ) } 

Теперь наш оператор наконец работает! Мы еще не совсем закончили. Нам все еще нужно обновить симпатичный принтер AST (который возвращает AST обратно к коду). В настоящее время он не поддерживает наш оператор диапазона — это можно увидеть, используя его в assert() :

 assert ( 1 | > 2 ) ;  // segfaults 

Обратите внимание, что assert() использует симпатичный принтер, чтобы вывести выражение, являющееся частью его сообщения об ошибке при сбое. Это делается только тогда, когда заявленное выражение не в строковой форме (поскольку в противном случае красивый принтер не понадобился бы) и является новинкой в ​​PHP 7.

Чтобы исправить это, нам просто нужно обновить файл [Zend / zend_ast.c] ( http://lxr.php.net/xref/PHP_7_0/Zend/zend_ast.c ), чтобы превратить наш узел ZEND_AST_RANGE в строку. Сначала мы обновим комментарий таблицы приоритетов (строка ~ 520), указав нашему новому оператору приоритет 170 (это должно соответствовать файлу zend_language_parser.y):

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

Далее нам нужно вставить инструкцию zend_ast_export_ex функцию zend_ast_export_ex для обработки ZEND_AST_RANGE (чуть выше 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 ) ; 

Симпатичный принтер был обновлен и assert() снова работает нормально:

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

Вывод

Эта статья охватила много вопросов, хотя и тонко. Он показал этапы, которые ZE проходит при запуске сценариев PHP, как эти этапы взаимодействуют друг с другом, и как мы можем изменить каждый из этих этапов, чтобы включить новый оператор в PHP. Эта статья продемонстрировала только одну возможную реализацию оператора диапазона в PHP — я расскажу об альтернативной (и лучшей) реализации в дальнейшем.