Досліджуємо результат роботи php-транслятора

Доброго дня. Думаю, що більшість веб-програмістів знає, як працює php-інтерпретатор.

Для тих, хто не знає:
Спочатку, написаний нами код розбирається лексичним аналізатором. Далі, отримані лексеми, передаються в синтаксичний аналізатор. Якщо синтаксичний аналізатор дав добро, то лексеми передаються транслятору, а він, у свою чергу, генерує так звані opcodes (operation codes). І тільки після цього в справу вступає віртуальна машина PHP (та сама Zend Engine), яка і виконує наш алгоритм з одержаних opcodes. Opcodes так само називають таким php-шних ассемблером.
Дана стаття розповість вам про те, які opcodes і в яких випадках генеруються. Звичайно, розповісти про все opcodes в рамках однієї статті не вийде, але в цій статті буде розглянуто конкретний приклад і на його основі ми спробуємо розібратися що до чого у цих opcodes. На мій погляд, найголовніше, що ви дізнаєтеся прочитавши статтю, це те, як насправді відбувається виконання ваших вихідних текстів і, можливо, це допоможе вам у кращому розумінні мови php.

Раджу вам налити собі чашечку капучино або просто зеленого чаю, т. к. під катом лістинги opcodes і php-коду…

Постановка завдання
Не так давно, сидячи на одному з php-форумів, я натрапив на топік, в якому ТЗ просив допомогти йому скласти алгоритм, який би з рядка:
Привіт {{Віктор|{Антон|Антоніо|Антошка}|Сергій}|{Пан|Сер|Товариш}}, {твої|ваші} справи
Робив приблизно наступні:
Привіт Віктор, як твої справи
Як ви вже напевно зрозуміли, необхідно було знайти всі входження {...} і дістати звідти фрази розділені символом "|". Потім, всі входження {...} замінити на одну з тих фраз. Фразу потрібно було вибрати випадковим чином.
Тут чітко видно дві глобальні групи:
{{Віктор|{Антон|Антоніо|Антошка}|Сергій}|{Пан|Сер|Товариш}}
і
{твої|ваші}
За умов початкової задачі, рівень вкладеності ролі не грав, то є всі фрази, розділені символом "|" у межах глобальної групи, мали однакову вагу.
Я запропонував наступний варіант вирішення завдання:
$str = 'Привіт {{Віктор|{Антон|Антоніо|Антошка}|Сергій}|{Пан|Сер|Товариш}} {твої|ваші} справи';
$str = preg_replace_callback('#(\{[\s\S]+?\})([^\|\{\}]+)#', function($mathces)
{
$mathces[1] = str_replace(array('}','{'), ", $mathces[1]);
$arr = explode('|', $mathces[1]);
return $arr[array_rand($arr)].$mathces[2];
}, $str);
echo "$str\n";

Регулярний вираз просто вибирає всі послідовності типу:
{.....} якийсь текст
В даному випадку вийдуть 2 входження на 2 групи в кожному (групи виділені дужками):
( {{Віктор|{Антон|Антоніо|Антошка}|Сергій}|{Пан|Сер|Товариш}} ) ( )
і
( {твої|ваші} ) ( справи )
Очищаємо їх від "{" і "}", щоб можна було застосувати explode() до чистенької рядку. І в кінці замінюємо всю послідовність на випадкову фразу з групи, отриману через explode() і rand(). Тут, думаю, труднощів ні в кого не виникне, оскільки алгоритм досить простий.
Відразу скажу, що у цього варіанту є деякі недоліки. Ось деякі з них:
  • Якщо текст закінчується символом "}", то в кінці тексту потрібно вставити пробіл
  • Якщо поряд стоять дві пари фігурних дужок — {...}{...}, то між ними потрібен хоча б пробіл
Але це швидше недоліки складеної регулярки.
Пізніше я покажу ще 2 варіанти які позбавлені даних обмежень, а поки припустимо, що працюємо з ідеальним, для даної регулярки, текстом.
Давайте нарешті подивися, що ж нам нагенерировал транслятор:
Opcodes dump
filename: /www/patterns/www/scan/simple.php
function name: (null)
number of ops: 11
compiled vars: !0 = $str
line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > ASSIGN !0, 'тут багато символів'
2 1 SEND_VAL 'регулярка'
2 DECLARE_LAMBDA_FUNCTION 'ім'я анонімної функції'
7 3 SEND_VAL ~1
4 SEND_VAR !0
5 DO_FCALL 3 $2 'preg_replace_callback'
6 ASSIGN !0, $2
8 7 ADD_VAR ~4 !0
8 ADD_CHAR ~4 ~4, 10
9 ECHO ~4
10 > RETURN 1

Function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fsimple.php0x7f68d7e7c0f:
filename: /www/patterns/www/scan/simple.php
function name: {closure}
number of ops: 22
compiled vars: !0 = $mathces, !1 = $arr
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > RECV !0 
4 1 INIT_ARRAY ~1 '%7D'
2 ADD_ARRAY_ELEMENT ~1 '%7B'
3 SEND_VAL ~1
4 SEND_VAL "
5 FETCH_DIM_R $2 !0, 1
6 SEND_VAR $2
7 DO_FCALL 3 $3 'str_replace'
8 ASSIGN_DIM !0, 1
9 OP_DATA $3, $4
5 10 SEND_VAL '%7C'
11 FETCH_DIM_R $5 !0, 1
12 SEND_VAR $5
13 DO_FCALL 2 $6 'explode'
14 ASSIGN !1, $6
6 15 SEND_VAR !1
16 DO_FCALL 1 $8 'array_rand'
17 FETCH_DIM_R $9 !1, $8
18 FETCH_DIM_R $10 !0, 2
19 CONCAT ~11 $9, $10
20 > RETURN ~11
7 21* > RETURN null

End of function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fsimple.php0x7f68d7e7c0f.


«Ну ось, купа чогось незрозумілого» — можете сказати ви.
Але, запевняю вас, тут немає нічого складного, давайте розбиратися.
Відразу можна побачити, що дамп як би розділений на 2 частини:
  • 1 частина — дамп основного коду скрипта
  • 2 частина — дамп коду анонімної функції, яку ми передаємо в preg_replace_callback.


Досліджуємо opcodes
Коли ми пишемо наш код, ми використовуємо змінні для зберігання значень. Інтерпретатор PHP робить те ж саме коли розбирає наш код, тільки в трохи більшому обсязі.
Opcode, в межах скрипта або функції, має кілька характеристик:
  • Номер рядка у файлі
  • Порядковий номер opcode в скрипті або функції
  • Ім'я
  • Область видимості
  • Додаткова інформація (поки що знайшов їй тільки одне застосування якому коли-небудь розповім)
  • Внутрішня змінна в яку буде збережений результат роботи opcode
  • Операнди (дані) над якими opcode виробляє якісь дії
Якщо говорити простіше, то opcode бере операнди, виконує над ними якісь дії і повертає результат в зазначену внутрішню змінну.
Внутрішні змінні — це ділянки пам'яті, які виділяє інтерпретатор при виконання opcodes. Ці змінні мають порядкові номери, які починаються з нуля і маю 3 основних типи:
  • Фізичні (починається з символу "!")
  • Віртуальні (починається з символу "$")
  • Тимчасові (починається з символу "~")
Давайте почнемо з першого opcode (поки що розглядаємо першу частину дампа) і подивимося що вийде:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > ASSIGN !0, 'тут багато тексту'

Видно, що це перший рядок скрипта і нульовий по рахунку opcode. Судячи з назви, opcode ASSIGN присвоює одне значення іншим. Ось і в даному випадку ми присвоїли наш тестовий текст змінної !0, а судячи з дампу, !0 — це наша $str в скрипті (подивіться на перший рядок коду щоб переконатися, що відбувається привласнення тексту).
Opcode в даному випадку нічого не повертає, т. к. виконує присвоювання і результат вже буде зберігатися в змінної !0.
Тепер давайте розглянемо, що відбувається при виклику функції. Щоб викликати функцію, нам потрібно зрадити їй на вхід певні аргументи в якості параметрів. Власне цим і займаються opcodes під номерами 1,3 і 4
Давайте зіставимо аргументи, які ми передали в preg_replace_callback і відповідні opcodes:
  • Передаємо регулярку: SEND_VAL 'текст регулярки'
  • Оголошуємо лямбда-функцію: DECLARE_LAMBDA_FUNCTION 'внутрішнє ім'я анонімної функції'
  • Результат (функція) неявно міститься у внутрішню тимчасову змінну ~1
  • Передаємо лямбда-функцію: SEND_VAL ~1
  • Передаємо цільову рядок: SEND_VAR !0
Ви напевно помітили, що в одному випадку використовується SEND_VAL, а в іншому — SEND_VAR.
Пов'язано це з тим, що в одних випадках, в якості параметра, ми передаємо змінну (SEND_VAR), а в інших — чисте значення (SEND_VAL).
У відповідності з цим, текст регулряки і лямбду — ми передаємо як значення, а вихідну рядок — як змінну (якою вона і є).
Все, аргументи передані і тепер залишається тільки викликати функцію:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
5 DO_FCALL 3 $2 'preg_replace_callback'

Тут, opcode з ім'ям DO_FCALL викликає функцію 'preg_replace_callback' і поміщає результат її роботи у внутрішню змінну $2.
Зауважте, не в фізичну змінну $str (як написано в скрипті), а поки що саме у внутрішню.
Ну і щоб покласти результат в $str (!0), використовується вже знайомий нам ASSIGN:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
6 ASSIGN !0, $2

Присвоює значення $2 змінної !0 (наша $str). З цього випливає, що значення, що повертаються функціями не присвоюються чекають їх змінним безпосередньо, а тільки через тимчасову змінну.
Поїхали далі.
Остання рядок скрипта. Необхідно просто вивести отриману рядок, але ми бачимо тут аж 3 opcode. Давайте подивимося:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
8 7 ADD_VAR ~4 !0
8 ADD_CHAR ~4 ~4, 10
9 ECHO ~4

  • Додаємо до тимчасової змінної ~4 (зараз там нічого немає) нашу змінну !0 ($str)
  • Додаємо до тимчасової змінної ~4(в якій вже лежить значення !0) символ переносу рядка \n (код символу — 10)
  • Виводимо вміст ~4 (зауважте, що echo — це не функція, а оператор мови)
Власне, ось і весь основний скрипт.
Варто відзначити, що цей захід завершується через opcode RETURN (хоча функції ми поки що не розглядали). Це така особливість php — скрипт або функція обов'язково повинні повернути що-небудь в кінці, навіть якщо return явно не вказаний. Закінчення скриптів повертають одиницю, а функції повертають null (якщо не вказано що-небудь інше).
Тепер давайте розглянемо анонімну функцію (друга частина дампа).
Її логіка проста:
  • Чистимо вміст групи від "{" і "}"
  • Поділяємо її по символу "|"
  • Вибираємо випадковий варіант
Розглянемо opcodes і відразу звернемо увагу на compiled vars для даної функції.
Перший opcode — RECV
Він отримує переданий в функцію аргумент і кладе його вміст !0.
Тепер нам потрібно провести низку маніпуляцій для виклику функції str_replace():
  • Ініціалізувати масив з символами заміни і віддати його в якості першого параметра
  • Передати другий параметр — пустий рядок
  • Передати першу групу з входження в якості третього параметра


line # * op fetch ext return operands
---------------------------------------------------------------------------------
4 1 INIT_ARRAY ~1 '%7D'
2 ADD_ARRAY_ELEMENT ~1 '%7B'
3 SEND_VAL ~1

  • Ініціалізуємо масив поміщаючи в нього символ "}" (сам масив кладемо в ~1)
  • Додаємо до масиву символ "{", результат поміщаємо туди ж
  • Передаємо аргумент функції


line # * op fetch ext return operands
---------------------------------------------------------------------------------
4 SEND_VAL "

  • Передаємо в функцію пустий рядок


line # * op fetch ext return operands
---------------------------------------------------------------------------------
5 FETCH_DIM_R $2 !0, 1
6 SEND_VAR $2

  • Беремо з масиву !0 елемент з індексом 1 і поміщаємо результат в $2
  • Передаємо в функцію значення $2
З цього випливає, що читання значень масивів відбувається в 2 дії: читання під тимчасову змінну і вже потім використання вмісту цієї змінної.

line # * op fetch ext return operands
---------------------------------------------------------------------------------
7 DO_FCALL 3 $3 'str_replace'
8 ASSIGN_DIM !0, 1
9 OP_DATA $3, $4

Тут ми спочатку викликаємо функцію str_replace() і результат кладемо в $3.
Далі досить цікавий момент…
Виразної документації по ASSIGN_DIM немає, а по OP_DATA якої-небудь документації в принципі немає, тому можу припустити, що ASSIGN_DIM проектує елементи масиву на комірки пам'яті і повертає покажчик на область пам'яті, де знаходиться потрібний нам елемент. Покажчик неявно міститься у $4. В даному випадку, судячи з операндам і кодом, нас цікавить елемент з індексом 1 в масиві !0.
Далі, OP_DATA пише результат функції str_replace() з $3 в область пам'яті, покажчик на яку зберігається в $4.
Не зрозуміло чому в даному випадку не використовується ASSIGN. Мабуть це пов'язано зі специфікою зберігання даних у вигляді масиву. Хоча при модифікації покажчиків теж використовується ASSIGN (правда там ASSIGN_REF).
Якщо хто-то точно знає, що за зв'язка ASSIGN_DIM\OP_DATA і як вона дійсно працює, напишіть будь ласка в коментах, буду вдячний.
Далі по коду, ми викликаємо explode(), для того щоб розбити рядок по символу "|":
line # * op fetch ext return operands
---------------------------------------------------------------------------------
5 10 SEND_VAL '%7C'
11 FETCH_DIM_R $5 !0, 1
12 SEND_VAR $5
13 DO_FCALL 2 $6 'explode'
14 ASSIGN !1, $6

  • Відправляємо в якості параметра символ "|"
  • Зчитуємо з масиву !0 елемент з індексом 1 і записуємо результат в $5
  • Передаємо $5 в якості параметра
  • Викликаємо explode(), записуючи результат в $6
  • Копіюємо вміст $6 !1


Далі, ми вибираємо рендомний елемент з отриманого масиву:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
6 15 SEND_VAR !1
16 DO_FCALL 1 $8 'array_rand'
17 FETCH_DIM_R $9 !1, $8
18 FETCH_DIM_R $10 !0, 2
19 CONCAT ~11 $9, $10
20 > RETURN ~11

  • Відправляємо !1 (масив) на вхід функції array_rand(), яка, у свою чергу повертає результат в $8
  • Потім записуємо в $9 елемент масиву !1 з індексом $8
  • Записуємо в $10 елемент масиву !0 з індексом 2
  • Об'єднуємо $9 до $10 в ~11
  • Повертаємо вміст ~11
Зараз я свідомо не розписую все більш детально, оскільки вважаю, що це вже отже зрозуміло, адже я все це розписував раніше.
Ось ми власне і розібрали наш перший варіант алгоритму, що називається, по кісточках.
Тепер давайте рушимо далі.

Підтримка екранованих символів в тексті
Трохи подумавши, я вирішив, що в тексті може бути будь-який порядок слів, не обов'язково {...} текст. Та й у тексті можуть зустрічатися такі символи як "{", "}" і "|" які не повинні бути оброблені роздільниками або групи варіантів.
Виходячи з цієї задумки, вирішив, що якщо необхідно використовувати "{", "}" і "|" у тексті, то їх треба просто заекранувати слешем, наприклад "\{".
Та й безглузді обмеження типу необхідності прогалин в кінці рядка і між стоять поруч групами, трохи напружували.
Склав собі наступну тестову рядок:
В мові {{C++|C}|{JavaScript|PHP}|C#|Java}, блоки коду можна об'єднувати в фігурні дужки, наприклад \{{ВАШ КОД|ЯКИЙСЬ КОД};\}\пУсловия записуються так {if(1)|if(1\|\|0)}{\{do_something();\}|\{do_some_work();\}}
З якої повинно вийде щось на кшталт:
В мові Java, блоки коду можна об'єднувати в фігурні дужки, наприклад {ВАШ КОД;}
Умови записуються так if(1||0){do_some_work();}
Для реалізації такого варіанту, треба було переробити регулярний вираз.
Саме під час обдумування даного варіанту, я набив руку у використанні тверджень в регулярних виразах. Звідси висновок — придумуйте собі задачі, для вирішення яких потрібно мати знання, яких у вас немає. Або є, але не було часу їх зміцнити.
Вийшло наступне:
$str = 'В мові {{C++|C}|{JavaScript|PHP}|C#|Java} блоки коду можна об'єднувати в фігурні дужки, наприклад \{{ВАШ КОД|ЯКИЙСЬ КОД};\}<br>Умови записуються так {if(1)|if(1\|\|0)}{\{do_something();\}|\{do_some_work();\}}';
$str = preg_replace_callback('#(?<!\\\)(\{[\s\S]+?(?<!\\\)\})(?![\|\}])#', function($mathces)
{
$mathces[1] = preg_replace('#(?<!\\\)\{|(?<!\\\)\}#', ", $mathces[1]);
$arr = preg_split('#(?<!\\\)\|#', $mathces[1]);
return $arr[array_rand($arr)];
}, $str);
$str = str_replace(array('\{', '\}', '\|'), array('{', '}', '|'), $str)."\n";
echo $str;

Регулярка працює наступним чином:
  • Знаходимо входження символу "{" перед яким не варто слеш (це показник того, що символ не екранований, а значить є початком групи варіантів)
  • Забираємо будь-які символи до символу "}", перед яким немає слешу. Це означає, що навіть якщо нам попадуться екрановані "}", вони не будуть вважатися кінцем групи, т. к. за умовою, ми забираємо всі символи до не екранованого "}"
  • (?![\|\}]) — цим твердженням ми як би говоримо, що після закривається символу "}" не повинно бути символів "|" і "}". Тим самим, ми хочемо бути впевнені, що обробляємо групу мінімального рівня вкладеності (тобто глобальну групу)
Так, цей варіант не дію обмеження минулого варіанту, але й у нього є свої тонкощі. Наприклад, дану регулярку можна легко звести з толку, вставивши після будь-якого закриває символу "}", символ "|".
Наприклад:
В мові {{C++|C}|{JavaScript|PHP}|C#|Java}| блоки коду...........
Якщо так зробити, то алгоритм просто зжере частина тексту, вважаючи його варіантом.
Але тут, як і в минулому варіанті, справа, швидше за все, складеної в регулярці.
Пізніше ми побачимо як обійти і це обмеження. А поки, давайте подивимося, що ще змінилося в алгоритмі і розглянемо дамп opcodes.
Отже, в даному варіанті, крім регулярки, змінилося ще й вміст лямбды і кінець скрипта.
В кінці скрипта, ми, з допомогою preg_replace(), прибираємо все не екрановані слеші, щоб перетворити такі місця як:
\{do_something();\}
в
{do_something();}
В лямбда, з допомогою preg_replace(), ми чистимо рядок від екранованих символів "{" і "}", щоб залишилися тільки голі варіанти, розділені символом "|".
Потім, через preg_split(), ми отримуємо тільки ті фрази, які розділені символом "|", якому не передує слеш, тобто екранований "|" не буде вважатися роздільником.
Ну і повертаємо результат у вигляді рандомного елемента масиву.
Opcodes даного прикладу
filename: /www/patterns/www/scan/advence.php
function name: (null)
number of ops: 21
compiled vars: !0 = $str
line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > ASSIGN !0, 'багато символів'
2 DECLARE_LAMBDA_FUNCTION 'внутрішнє ім'я анонімної функції'
7 3 SEND_VAL ~1
4 SEND_VAR !0
5 DO_FCALL 3 $2 'preg_replace_callback'
6 ASSIGN !0, $2
8 7 INIT_ARRAY ~4 '%5C%7B'
8 ADD_ARRAY_ELEMENT ~4 '%5C%7D'
9 ADD_ARRAY_ELEMENT ~4 '%5C%7C'
10 SEND_VAL ~4
11 INIT_ARRAY ~5 '%7B'
12 ADD_ARRAY_ELEMENT ~5 '%7D'
13 ADD_ARRAY_ELEMENT ~5 '%7C'
14 SEND_VAL ~5
15 SEND_VAR !0
16 DO_FCALL 3 $6 'str_replace'
17 CONCAT ~7 $6, '%0A'
18 ASSIGN !0, ~7
9 19 ECHO !0
20 > RETURN 1

Function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fadvence.php0x7f0b122bb19:
filename: /www/patterns/www/scan/advence.php
function name: {closure}
number of ops: 18
compiled vars: !0 = $mathces, !1 = $arr
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 0 > RECV !0 
4 1 SEND_VAL '%23%28%3F%3C%21%5C%5C%29%5C%7B%7C%28%3F%3C%21%5C%5C%29%5C%7D%23'
2 SEND_VAL "
3 FETCH_DIM_R $1 !0, 1
4 SEND_VAR $1
5 DO_FCALL 3 $2 'preg_replace'
6 ASSIGN_DIM !0, 1
7 OP_DATA $2, $3
5 8 SEND_VAL '%23%28%3F%3C%21%5C%5C%29%5C%7C%23'
9 FETCH_DIM_R $4 !0, 1
10 SEND_VAR $4
11 DO_FCALL 2 $5 'preg_split'
12 ASSIGN !1, $5
6 13 SEND_VAR !1
14 DO_FCALL 1 $7 'array_rand'
15 FETCH_DIM_R $8 !1, $7
16 > RETURN $8
7 17* > RETURN null

End of function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fadvence.php0x7f0b122bb19.


Спробуйте самі розібрати цей дамп, тут нічого нового немає.
Ну ось я і розповів про другий варіант. Залишився останній і самий цікавий, як з точки зору регулярки, так і з точки зору розгляду opcodes.

Рекурсія
Після того, як дискусія на гілці форуму була припинена, в топік прийшов якийсь товариш і сказав, що мовляв «задачка вже стара і вирішували її давно» і навів приклад пропозиції, яке треба було розібрати таким чином:
{Ласка|Просто} зробіть так, щоб ця пропозиція {змінювалося {Швидко|Миттєво} випадковим чином}
І тут мені стало зрозуміло, що потрібно вводити підтримку рівня вкладеності групи варіантів, а отже — потрібна рекурсія. Причому зрозуміло, що кількість проходів одно максимального рівня вкладеності в тексті.
Почав думати. В результаті чого і народився останній варіант, яким я задоволений. Барабанний дріб…
while(preg_match('#(?<!\\\)\{#', $str))
{
$str = preg_replace_callback('#(?<!\\\)\{((?(?!\\\)[^\{]+?|[\s\S]+?))(?<!\\\)\}#', function($mathces)
{
$arr = preg_split('#(?<!\\\)\|#', $mathces[1]);
return $arr[array_rand($arr)];
}, $str);
}
$str = str_replace(array('\{', '\}', '\|'), array('{', '}', '|'), $str);
echo $str;

Єдине, я придумав свій варіант вихідної рядки, в якому було б задіяно все, що тільки можна:
{{{вранці і після обіду}}|Вчора} я {побіг|пішов|поїхав{ на автобусі| на машині| {трамваї|тролейбусі}|}} {зоо-|комп'ютерний|інтимний|продуктовий} магазин щоб {купити|вкрасти} {костюм {сови|{людини павука|бетмена}|Вінні-Пуха|колобка}|презерватив}
Після обробки, дана рядок, по ідеї, може перетворитися в щось таке:
Сьогодні вранці я побіг в зоо — магазин щоб вкрасти презерватив
Спочатку, давайте розглянемо регулярку і лямбду.
Знаходимо входження не екранованого символу "{"
Далі, нам треба забрати будь-які символи до першого не екранованого символу "}", тобто, кажучи іншими словами, ми шукаємо всі найглибші групи
Паркан відбувається за умові. Умова побудовано таким чином, щоб механізм регулярних виразів точно визначав початок і кінець рядка, як би не залишаючи йому іншого вибору. Без такої умови, були б проблеми з рядками типу:
Привіт {{Віктор|{Антон|Антоніо|Антошка}|Сергій}|{Пан|Сер|Товариш}} {твої|ваші} справи
Т. к. в даному випадку, регулярний вираз вважала б початком групи — "{Віктор", а насправді, початком першої групи є "{Антон".
Загалом вся ця круговерть продовжується до тих пір, поки в тексті залишилися не екрановані "{". Тобто після кожного проходу preg_replace_callback() — відбувається підйом рівня вкладеності і процес повторюється до настання зазначеного вище події.
Звичайно, ви можете сказати, що все це можна зробити і більш простим регулярним виразом, але згадайте про підтримку "{", "}" і "|".
Так, вона тут теж є. І що найголовніше — відсутні обмеження на формат тексту (на скільки я міг помітити під час тестування алгоритму).
Тепер давайте розберемо те, заради чого ми тут усі зібралися.
Opcode dump
filename: /www/patterns/www/scan/advence2.php
function name: (null)
number of ops: 25
compiled vars: !0 = $str
line # * op fetch ext return operands
---------------------------------------------------------------------------------
1 0 > ASSIGN !0, 'багато символів'
2 1 > SEND_VAL регулярка для перевірки на існування в рядку не екранованих "{"
2 SEND_VAR !0
3 DO_FCALL 2 $1 'preg_match'
4 > JMPZ $1, ->12
4 5 > SEND_VAL 'довга регулярка'
6 DECLARE_LAMBDA_FUNCTION 'внутрішнє ім'я анонімної функції'
8 7 SEND_VAL ~2
8 SEND_VAR !0
9 DO_FCALL 3 $3 'preg_replace_callback'
10 ASSIGN !0, $3
9 11 > JMP ->1
10 12 > INIT_ARRAY ~5 '%5C%7B'
13 ADD_ARRAY_ELEMENT ~5 '%5C%7D'
14 ADD_ARRAY_ELEMENT ~5 '%5C%7C'
15 SEND_VAL ~5
16 INIT_ARRAY ~6 '%7B'
17 ADD_ARRAY_ELEMENT ~6 '%7D'
18 ADD_ARRAY_ELEMENT ~6 '%7C'
19 SEND_VAL ~6
20 SEND_VAR !0
21 DO_FCALL 3 $7 'str_replace'
22 ASSIGN !0, $7
11 23 ECHO !0
24 > RETURN 1

Function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fadvence2.php0x7f733b71526:
filename: /www/patterns/www/scan/advence2.php
function name: {closure}
number of ops: 11
compiled vars: !0 = $mathces, !1 = $arr
line # * op fetch ext return operands
---------------------------------------------------------------------------------
4 0 > RECV !0 
6 1 SEND_VAL '%23%28%3F%3C%21%5C%5C%29%5C%7C%23'
2 FETCH_DIM_R $0 !0, 1
3 SEND_VAR $0
4 DO_FCALL 2 $1 'preg_split'
5 ASSIGN !1, $1
7 6 SEND_VAR !1
7 DO_FCALL 1 $3 'array_rand'
8 FETCH_DIM_R $4 !1, $3
9 > RETURN $4
8 10* > RETURN null

End of function %00%7Bclosure%7D%2Fwww%2Fpatterns%2Fwww%2Fscan%2Fadvence2.php0x7f733b71526.


Лямбду розбирати не будемо, вона проста до неподобства, а ось основний код розберемо, тим більше що я вже чую здивовані і повні інтересу вигуки, які прямо таки запитують розповісти їм про що з'явилися стрілочки (хоча ми бачили їх і раніше) і новий для нас вид opcodes — JMP*
Давайте по порядку. Звернемо увагу на:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
2 1 > SEND_VAL 'регулярка для перевірки на існування в рядку не екранованих "{"'
2 SEND_VAR !0
3 DO_FCALL 2 $1 'preg_match'
4 > JMPZ $1, ->12

Це while в нашому коді. Тут ми передаємо в функцію preg_match() аргументи у вигляді регулярки і рядка, яку будемо перевіряти на входження. Викликаємо функцію, поміщаємо результат в $1.
Тепер уважно. Opcode JPMZ — це керуючий opcode, який робить наступне:
Якщо операнд дорівнює нулю, то opcode передає керування в інше місце, бо JMPZ розшифровується як «Jump If Zero».
Судячи з дампу, ми можемо сміливо сказати, що якщо вміст $1 буде дорівнює 0, то управління перейде до opcode під номером 12.
А що там?
line # * op fetch ext return operands
---------------------------------------------------------------------------------
10 12 > INIT_ARRAY ~5 '%5C%7B'

А там 10 рядок нашого скрипта, тобто ми вже знаходимося за межами while.
Тепер згадуємо як працює while.
Якщо умовний вираз, яке ми передаємо в while, поверне нам що-небудь відмінне від нуля, то виконується тіло while, але якщо воно поверне на 0 або еквівалент (порожній рядок, порожній масив, false і т. д.), то нас викине з while. Що ми тут власне і спостерігаємо.
Ще раз погляньте на ділянку opcodes відповідають за реалізацію while.
Якщо те, що повернула нам функція preg_match() дорівнює 0, то вийти з циклу. preg_match() поверне нам 0, якщо збігу не були знайдені і нас викине з while.
Далі подивіться на ще один JMP:
line # * op fetch ext return operands
---------------------------------------------------------------------------------
9 11 > JMP ->1

Це, так звана, безумовна передача керування, перебуває вона, як бачимо, на 9 рядку скрипта. А там у нас закриває дужка while. Що станеться? Правильно! Чергова перевірка умови перед тілом циклу. Дивимося на opcode — JMP відправляє нас до opcode під номером 1. А що там? А там те, що ми вже розглядали.
Ось власне і вся логіка. Зверніть увагу, що перед іменами деяких opcodes, є стрілки вказують направо. Але тут нас цікавить не напрям стрілок і а їх розташування. Якщо стрілка розташована по лівому краю свого стовпця, то це означає вхід в ділянку коду. Це може бути мета умовного або безумовного переходу, або просто відкривається командна дужка.
Стрілки вирівняні по праву сторону, означають вихід з ділянки коду. Це може бути умовний чи безумовний перехід, або закривається, командна дужка. Так само зверніть увагу, що перед opcode RETURN стрілка притиснута до правого краю. Думаю тут не потрібно нічого пояснювати.

Ну що, ось власне і все, про що я хотів розповісти в цій статті.
Звичайно, можна було б замерит продуктивність всіх трьох варіантів (і я це робив), але, запевняю вас, що все залежить від завдання та на різних текстах продуктивність буде різна.

Для дампа opcodes, використовувався модуль VLD
PHP версії 5.5.14
Інші посилання вказані в тексті.

Сподіваюся, що дав вам нові знання та ви змогли краще зрозуміти роботу php.

Спасибі за увагу і всього вам найкращого!

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.