Spec-Zone.ru › Perl 5.28

perlinterp

СОДЕРЖАНИЕ

  • НАЗВАНИЕ
  • ОПИСАНИЕ
  • ЭЛЕМЕНТЫ ИНТЕРПРЕТАТОРА
    • Запуск
    • Парсинг
    • Оптимизация
    • Выполнение
    • Обработка исключений
    • ВНУТРЕННИЕ ТИПЫ ПЕРЕМЕННЫХ
  • ДЕРЕВЬЯ ОПЕРАЦИЙ
  • СТЕКИ
    • Стек аргументов
    • Стек меток
    • Стек сохранения
  • МИЛЛИОНЫ МАКРОСОВ
  • ДОПОЛНИТЕЛЬНАЯ ЛИТЕРАТУРА

НАЗВАНИЕ

perlinterp - Обзор интерпретатора Perl

ОПИСАНИЕ

Этот документ предоставляет обзор работы интерпретатора Perl на уровне кода C, а также ссылки на соответствующие файлы исходного кода C.

ЭЛЕМЕНТЫ ИНТЕРПРЕТАТОРА

Работа интерпретатора проходит в два основных этапа: компиляция кода во внутреннее представление, или байткод, и затем его выполнение. "Компилируемый код" в perlguts подробно описывает, как происходит этап компиляции.

Вот краткое описание работы Perl:

Запуск

Действие начинается в файле perlmain.c. (или miniperlmain.c для miniperl) Это код очень высокого уровня, который помещается на один экран, и он похож на код в perlembed; большая часть реального действия происходит в perl.c

perlmain.c генерируется ExtUtils::Miniperl из miniperlmain.c во время сборки, поэтому вам следует собрать Perl, чтобы следовать этому.

Сначала perlmain.c выделяет некоторую память и создаёт интерпретатор Perl, примерно так:

1 PERL_SYS_INIT3(&argc,&argv,&env);
2
3 if (!PL_do_undump) {
4     my_perl = perl_alloc();
5     if (!my_perl)
6         exit(1);
7     perl_construct(my_perl);
8     PL_perl_destruct_level = 0;
9 }

Строка 1 — макрос, и его определение зависит от вашей операционной системы. Строка 3 ссылается на PL_do_undump, глобальную переменную — все глобальные переменные в Perl начинаются с PL_. Это говорит о том, был ли текущий запущенный процесс создан с флагом -u для perl и затем undump, что означает, что он будет ложным в любом разумном контексте.

Строка 4 вызывает функцию в perl.c для выделения памяти для интерпретатора Perl. Это довольно простая функция, и её суть выглядит так:

my_perl = (PerlInterpreter*)PerlMem_malloc(sizeof(PerlInterpreter));

Здесь вы видите пример абстракции системы Perl, которую мы увидим позже: PerlMem_malloc — это либо системная malloc, либо собственная malloc Perl, определённая в malloc.c, если вы выбрали этот вариант при настройке.

Далее, в строке 7, мы создаём интерпретатор с помощью perl_construct, также в perl.c; это устанавливает все специальные переменные, необходимые для Perl, стеки и т. д.

Теперь мы передаём Perl параметры командной строки и говорим ему начать:

if (!perl_parse(my_perl, xs_init, argc, argv, (char **)NULL))
    perl_run(my_perl);

exitstatus = perl_destruct(my_perl);

perl_free(my_perl);

perl_parse фактически является оболочкой вокруг S_parse_body, как определено в perl.c, которая обрабатывает параметры командной строки, настраивает все статически связанные XS-модули, открывает программу и вызывает yyparse для её парсинга.

Парсинг

Цель этого этапа — взять исходный код Perl и преобразовать его в дерево операций. Мы увидим, как выглядит одно из них позже. Строго говоря, здесь происходит три вещи.

yyparse, парсер, находится в perly.c, хотя вам лучше прочитать исходный файл YACC в perly.y. (Да, в Perl есть грамматика YACC!) Задача парсера — взять ваш код и "понять" его, разделив его на предложения, определив, какие операнды соответствуют каким операторам и так далее.

Парсеру помогает лексический анализатор, который разбивает ваш ввод на токены и определяет тип каждого токена: имя переменной, оператор, оголённое слово, подпрограмма, внутренняя функция и так далее. Основной входной точкой в лексический анализатор является yylex, и её связанные функции находятся в toke.c. Perl сильно отличается от других языков программирования; он иногда сильно зависит от контекста, поэтому сложно определить тип токена или где он заканчивается. В связи с этим между лексическим анализатором и парсером много взаимодействий, что может быть довольно пугающим, если вы к этому не привыкли.

По мере понимания парсером программы Perl он строит дерево операций для интерпретатора во время выполнения. Функции, которые строят и связывают различные операции, находятся в op.c и будут рассмотрены позже.

Оптимизация

Теперь этап парсинга завершен, и готовое дерево представляет собой операции, которые интерпретатор Perl должен выполнить для выполнения нашей программы. Затем Perl выполняет прогон по дереву в поисках оптимизаций: константные выражения, такие как 3 + 4, будут вычислены сейчас, и оптимизатор также проверит, можно ли заменить несколько операций одной. Например, чтобы получить доступ к переменной $foo, вместо того, чтобы получить доступ к глобальной переменной *foo и посмотреть на скалярную часть, оптимизатор изменяет дерево операций, чтобы использовать функцию, которая непосредственно ищет нужный скаляр. Основной оптимизатор — peep в op.c, и многие операции имеют свои функции оптимизации.

Выполнение

Теперь мы, наконец, готовы: у нас есть скомпилированный байткод Perl, и всё, что осталось сделать, — это запустить его. Фактическое выполнение выполняется функцией runops_standard в run.c; точнее, это делается этими тремя безобидными строками:

while ((PL_op = PL_op->op_ppaddr(aTHX))) {
    PERL_ASYNC_CHECK();
}

Вам может быть удобнее использовать Perl-версию:

PERL_ASYNC_CHECK() while $Perl::op = &{$Perl::op->{function}};

Ну, может быть, и нет. В любом случае, каждая операция содержит указатель на функцию, которая указывает на функцию, которая фактически выполняет операцию. Эта функция вернёт следующую операцию в последовательности — это позволяет выполнять такие вещи, как if , которые выбирают следующую операцию динамически во время выполнения. PERL_ASYNC_CHECK гарантирует, что такие вещи, как сигналы, прерывают выполнение, если это необходимо.

Фактические вызываемые функции известны как PP-код, и они распределены между четырьмя файлами: pp_hot.c содержит «горячий» код, который чаще всего используется и оптимизирован, pp_sys.c содержит все функции, специфичные для системы, pp_ctl.c содержит функции, которые реализуют управляющие структуры (if, while и т. д.), а pp.c содержит всё остальное. Это, если хотите, код C для встроенных функций и операторов Perl.

Обратите внимание, что каждая pp_ функция должна возвращать указатель на следующую операцию. Вызовы подпрограмм Perl (и блоков eval) обрабатываются в том же цикле runops и не занимают дополнительное место в стеке C. Например, pp_entersub и pp_entertry просто помещают структуру блока CxSUB или CxEVAL в стек контекста, содержащую адрес операции, следующей за вызовом подпрограммы или eval. Затем они возвращают первую операцию этого подпрограммы или блока eval, и выполнение продолжается в этой подпрограмме или блоке. Позже операция pp_leavesub или pp_leavetry извлекает структуру блока CxSUB или CxEVAL, извлекает возвращающую операцию из неё и возвращает её.

Обработка исключений

Обработка исключений в Perl (т. е. die и т. д.) основана на низкоуровневых функциях setjmp()/longjmp() библиотеки C. В основном они обеспечивают способ захвата текущих регистров PC и SP и последующего их восстановления; т. е. longjmp() продолжается в той точке кода, где ранее была выполнена setjmp(), при этом всё, что выше в стеке C, теряется. Вот почему код всегда должен сохранять значения с помощью SAVE_FOO вместо автоматических переменных.

Ядро Perl оборачивает setjmp() и т. д. в макросы JMPENV_PUSH и JMPENV_JUMP. Основное правило исключений Perl состоит в том, что exit, и die (при отсутствии eval) выполняют JMPENV_JUMP(2), в то время как die внутри eval выполняет JMPENV_JUMP(3).

В точках входа в Perl, таких как perl_parse(), perl_run() и call_sv(cv, G_EVAL), каждый выполняет JMPENV_PUSH, затем входит в цикл runops или что-то подобное и обрабатывает возможные возвращаемые значения исключений. При возвращении 2 выполняется заключительная очистка, например, извлечение стеков и вызов блоков CHECK или END. Среди прочего, именно так происходит очистка области видимости при exit.

Если die может найти блок CxEVAL в стеке контекста, то стек извлекается до этого уровня, и возвращающая операция в этом блоке присваивается PL_restartop; затем выполняется JMPENV_JUMP(3). Это обычно возвращает управление обратно защите. В случае perl_run и call_sv, ненулевой PL_restartop запускает повторный вход в цикл runops. Это обычный способ обработки die или croak внутри eval.

Иногда операции выполняются внутри внутреннего цикла runops, например, код привязки, сортировки или перегрузки. В этом случае что-то вроде

sub FETCH { eval { die } }

вызовет longjmp прямо обратно в защитник в perl_run, извлекая оба цикла runops, что очевидно неправильно. Один из способов избежать этого — для кода привязки выполнить JMPENV_PUSH перед выполнением FETCH во внутреннем цикле runops, но по причинам эффективности Perl фактически устанавливает флаг, используя CATCH_SET(TRUE). Операции pp_require, pp_entereval и pp_entertry проверяют этот флаг, и если он истинный, они вызывают docatch, который выполняет JMPENV_PUSH и запускает новый уровень цикла runops для выполнения кода, а не делает это в текущем цикле.

В качестве дополнительной оптимизации при выходе из блока eval в FETCH, выполнение кода после блока всё ещё продолжается во внутреннем цикле. Когда возникает исключение, docatch сравнивает уровень JMPENV блока CxEVAL с PL_top_env и если они различаются, просто перебрасывает исключение. Таким образом, любые внутренние циклы извлекаются.

Вот пример.

1: eval { tie @a, 'A' };
2: sub A::TIEARRAY {
3:     eval { die };
4:     die;
5: }

Для запуска этого кода вызывается perl_run, который выполняет JMPENV_PUSH, а затем входит в цикл runops. Этот цикл выполняет операции eval и tie на строке 1, при этом eval помещает CxEVAL в стек контекста.

pp_tie выполняет CATCH_SET(TRUE), затем запускает второй цикл runops для выполнения тела TIEARRAY. Когда он выполняет операцию entertry на строке 3, CATCH_GET имеет значение true, поэтому pp_entertry вызывает docatch, который выполняет JMPENV_PUSH и запускает третий цикл runops, который затем выполняет операцию die. В этот момент стек вызовов C выглядит следующим образом:

Perl_pp_die
Perl_runops      # third loop
S_docatch_body
S_docatch
Perl_pp_entertry
Perl_runops      # second loop
S_call_body
Perl_call_sv
Perl_pp_tie
Perl_runops      # first loop
S_run_body
perl_run
main

а стеки контекста и данных, как показано -Dstv, выглядят следующим образом:

STACK 0: MAIN
  CX 0: BLOCK  =>
  CX 1: EVAL   => AV()  PV("A"\0)
  retop=leave
STACK 1: MAGIC
  CX 0: SUB    =>
  retop=(null)
  CX 1: EVAL   => *
retop=nextstate

Операция die извлекает первый CxEVAL из стека контекста, устанавливает значение PL_restartop из него, выполняет JMPENV_JUMP(3) и управление возвращается в верхний docatch. Затем он запускает другой цикл runops третьего уровня, который выполняет операции nextstate, pushmark и die на строке 4. В момент вызова второго pp_die, стек вызовов C выглядит точно так же, как выше, даже если мы больше не находимся внутри внутреннего eval; это из-за упомянутой ранее оптимизации. Однако стек контекста теперь выглядит так, то есть с извлеченным верхним CxEVAL:

STACK 0: MAIN
  CX 0: BLOCK  =>
  CX 1: EVAL   => AV()  PV("A"\0)
  retop=leave
STACK 1: MAGIC
  CX 0: SUB    =>
  retop=(null)

Операция die на строке 4 возвращает стек контекста обратно к CxEVAL, оставив его как:

STACK 0: MAIN
  CX 0: BLOCK  =>

Как обычно, PL_restartop извлекается из CxEVAL, и выполняется JMPENV_JUMP(3), которая возвращает стек C обратно к docatch:

S_docatch
Perl_pp_entertry
Perl_runops      # second loop
S_call_body
Perl_call_sv
Perl_pp_tie
Perl_runops      # first loop
S_run_body
perl_run
main

В этом случае, так как уровень JMPENV, записанный в CxEVAL, отличается от текущего, docatch просто выполняет JMPENV_JUMP(3), и стек C разворачивается до:

perl_run
main

Поскольку PL_restartop не равно null, run_body запускает новый цикл runops, и выполнение продолжается.

ТИПЫ ВНУТРЕННИХ ПЕРЕМЕННЫХ

К этому моменту вы, вероятно, уже ознакомились с perlguts, который рассказывает о внутренних типах переменных Perl: SVs, HVs, AVs и прочих. Если нет, сделайте это сейчас.

Эти переменные используются не только для представления переменных пространства Perl, но и любых констант в коде, а также некоторых структур, полностью внутренних для Perl. Например, таблица символов является обычным Perl-хэшем. Ваш код представлен как SV при чтении в анализатор; любые вызываемые вами файлы программ открываются с помощью обычных Perl-дескрипторов файлов и так далее.

Ядро модуля Devel::Peek позволяет нам просматривать SVs из программы Perl. Давайте, например, посмотрим, как Perl обрабатывает константу "hello".

  % perl -MDevel::Peek -e 'Dump("hello")'
1 SV = PV(0xa041450) at 0xa04ecbc
2   REFCNT = 1
3   FLAGS = (POK,READONLY,pPOK)
4   PV = 0xa0484e0 "hello"\0
5   CUR = 5
6   LEN = 6

Чтение вывода Devel::Peek требует некоторой практики, поэтому давайте пройдемся по нему строка за строкой.

Строка 1 сообщает нам, что мы смотрим на SV, расположенный в памяти по адресу 0xa04ecbc. Сами SVs представляют собой очень простые структуры, но они содержат указатель на более сложную структуру. В этом случае это PV, структура, которая содержит строковое значение, по адресу 0xa041450. Строка 2 — счётчик ссылок; других ссылок на эти данные нет, поэтому он равен 1.

Строка 3 — флаги для этого SV — его можно использовать как PV, это read-only SV (поскольку это константа), и данные внутри представлены как PV. Далее у нас содержимое строки, начиная с адреса 0xa0484e0.

Строка 5 дает нам текущую длину строки — обратите внимание, что это не включает завершающий нуль. Строка 6 — это не длина строки, а длина текущего выделенного буфера; по мере роста строки Perl автоматически расширяет доступное хранилище с помощью процедуры, называемой SvGROW.

Вы можете очень легко получить доступ к любой из этих величин из C; просто добавьте Sv к имени поля, показанного в фрагменте, и у вас есть макрос, который вернёт значение: SvCUR(sv) возвращает текущую длину строки, SvREFCOUNT(sv) — счётчик ссылок, SvPV(sv, len) возвращает саму строку с её длиной и так далее. Более подробные макросы для манипулирования этими свойствами можно найти в perlguts.

Давайте рассмотрим пример манипулирования PV, взятый из sv_catpvn, в sv.c

 1  void
 2  Perl_sv_catpvn(pTHX_ SV *sv, const char *ptr, STRLEN len)
 3  {
 4      STRLEN tlen;
 5      char *junk;

 6      junk = SvPV_force(sv, tlen);
 7      SvGROW(sv, tlen + len + 1);
 8      if (ptr == junk)
 9          ptr = SvPVX(sv);
10      Move(ptr,SvPVX(sv)+tlen,len,char);
11      SvCUR(sv) += len;
12      *SvEND(sv) = '\0';
13      (void)SvPOK_only_UTF8(sv);          /* validate pointer */
14      SvTAINT(sv);
15  }

Это функция, которая добавляет строку ptr длиной len в конец PV, хранящегося в sv. Первое, что мы делаем на строке 6, это убеждаемся, что SV имеет допустимый PV, вызвав макрос SvPV_force для принудительного создания PV. В качестве побочного эффекта tlen устанавливается в текущее значение PV, а сам PV возвращается в junk.

На строке 7 мы убеждаемся, что у SV будет достаточно места для размещения старой строки, новой строки и завершающего нуля. Если LEN недостаточно, SvGROW перевыделит память для нас.

Теперь, если junk совпадает со строкой, которую мы пытаемся добавить, мы можем напрямую получить строку из SV; SvPVX — это адрес PV в SV.

Строка 10 выполняет фактическое конкатенацию: макрос Move перемещает кусок памяти: мы перемещаем строку ptr в конец PV — это начало PV плюс его текущая длина. Мы перемещаем len байтов типа char. После этого нам нужно сообщить Perl о расширении строки, изменив CUR для отражения новой длины. SvEND — это макрос, который даёт нам конец строки, поэтому он должен быть "\0".

Строка 13 изменяет флаги; поскольку мы изменили PV, любые значения IV или NV больше не будут действительными: если у нас есть $a=10; $a.="6";, мы не хотим использовать старое IV 10. SvPOK_only_utf8 — это специальная версия макроса SvPOK_only, учитывающая UTF-8, которая отключает флаги IOK и NOK и включает POK. Конечный SvTAINT — это макрос, который очищает данные, если включен режим taint.

AV и HV более сложны, но SV — это, безусловно, самый распространённый тип переменной, с которым приходится работать. Увидя как мы работаем с ними, давайте перейдем к тому, как построено дерево op.

ДЕРЕВЬЯ OP

Во-первых, что такое дерево op? Дерево op — это представленная в виде дерева структура вашего программы, как мы видели в разделе по синтаксическому анализу, и это последовательность операций, которые Perl выполняет, чтобы выполнить вашу программу, как мы видели в "Running".

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

Дерево op соединяется двумя способами: можно представить, что в нём есть два «маршрута», два порядка, в которых можно обойти дерево. Во-первых, порядок синтаксического анализа отражает, как анализатор понял код, а во-вторых, порядок выполнения показывает, в каком порядке Perl выполняет операции.

Самый простой способ проверить дерево op — это остановить Perl после завершения синтаксического анализа и заставить его вывести дерево. Именно это делают компиляторы с бэкендами B::Terse, B::Concise и B::Debug.

Давайте посмотрим, как Perl видит $a = $b + $c:

 % perl -MO=Terse -e '$a=$b+$c'
 1  LISTOP (0x8179888) leave
 2      OP (0x81798b0) enter
 3      COP (0x8179850) nextstate
 4      BINOP (0x8179828) sassign
 5          BINOP (0x8179800) add [1]
 6              UNOP (0x81796e0) null [15]
 7                  SVOP (0x80fafe0) gvsv  GV (0x80fa4cc) *b
 8              UNOP (0x81797e0) null [15]
 9                  SVOP (0x8179700) gvsv  GV (0x80efeb0) *c
10          UNOP (0x816b4f0) null [15]
11              SVOP (0x816dcf0) gvsv  GV (0x80fa460) *a

Давайте начнём с середины, со строки 4. Это BINOP, бинарный оператор, находящийся по адресу 0x8179828. Конкретный оператор — это sassign — присваивание скаляру — и вы можете найти код, который его реализует, в функции pp_sassign в pp_hot.c. Как бинарный оператор, он имеет двух потомков: оператор сложения, предоставляющий результат $b+$c, находится в верхней части строки 5, а левая часть — на строке 10.

Строка 10 — это null-op: она ничего не делает. Что она там делает? Если вы видите null-op, это знак того, что что-то было оптимизировано после синтаксического анализа. Как мы упоминали в "Оптимизация", на этапе оптимизации иногда две операции объединяются в одну, например, при получении значения скалярной переменной. Когда это происходит, вместо переписывания дерева op и очистки висячих указателей проще заменить избыточную операцию на null-op. Изначально дерево выглядело бы так:

10          SVOP (0x816b4f0) rv2sv [15]
11              SVOP (0x816dcf0) gv  GV (0x80fa460) *a

То есть, получить запись a из основной таблицы символов, а затем посмотреть на скалярную составляющую: gvsv (pp_gvsv в pp_hot.c) как раз и делает обе эти вещи.

Правая часть, начиная со строки 5, аналогична тому, что мы только что видели: у нас есть оператор add (pp_add, также в pp_hot.c), который складывает два gvsv.

Теперь, что это значит?

1  LISTOP (0x8179888) leave
2      OP (0x81798b0) enter
3      COP (0x8179850) nextstate

enter и leave — это операторы области видимости, и их задача — выполнять все необходимые действия каждый раз при входе и выходе из блока: лексические переменные приводятся в порядок, неиспользуемые переменные уничтожаются и так далее. В каждой программе будут эти три строки: leave — это список, а его потомки — все инструкции в блоке. Инструкции разделяются nextstate, поэтому блок — это набор операторов nextstate, а операции, выполняемые для каждой инструкции, являются потомками nextstate . enter — это единственная операция, которая выполняет роль метки.

Вот как Perl анализирует программу сверху вниз:

 Program
    |
Statement
    |
    =
   / \
  /   \
 $a   +
     / \
   $b   $c

Однако невозможно выполнять операции в этом порядке: например, необходимо получить значения $b и $c перед их сложением. Таким образом, другой поток, который проходит через дерево op, — это порядок выполнения: каждый op имеет поле op_next, которое указывает на следующий op, который нужно запустить, поэтому, следуя этим указателям, мы можем увидеть, как Perl выполняет код. Мы можем обходить дерево в этом порядке, используя параметр exec для B::Terse:

% perl -MO=Terse,exec -e '$a=$b+$c'
1  OP (0x8179928) enter
2  COP (0x81798c8) nextstate
3  SVOP (0x81796c8) gvsv  GV (0x80fa4d4) *b
4  SVOP (0x8179798) gvsv  GV (0x80efeb0) *c
5  BINOP (0x8179878) add [1]
6  SVOP (0x816dd38) gvsv  GV (0x80fa468) *a
7  BINOP (0x81798a0) sassign
8  LISTOP (0x8179900) leave

Это, вероятно, более понятно человеку: войти в блок, начать инструкцию. Получить значения $b и $c, и сложить их. Найти $a, и присвоить одно другому. Затем выйти.

Способ, которым Perl строит эти деревья op в процессе синтаксического анализа, можно изучить, изучив toke.c, лексический анализатор, и perly.y, грамматику YACC. Давайте посмотрим на код, который строит дерево для $a = $b + $c.

Сначала мы рассмотрим функцию Perl_yylex в лексере. Мы хотим найти case 'x', где x — первый символ оператора. (Кстати, при поиске кода, обрабатывающего ключевое слово, вам нужно будет искать KEY_foo, где «foo» — это ключевое слово.) Вот код, который обрабатывает присваивание (существует довольно много операторов, начинающихся с =, поэтому большая часть из них опущена для краткости):

1    case '=':
2        s++;
         ... code that handles == => etc. and pod ...
3        pl_yylval.ival = 0;
4        OPERATOR(ASSIGNOP);

Мы видим на строке 4, что наш тип токена ASSIGNOP (OPERATOR — макрос, определенный в toke.c, который, помимо прочего, возвращает тип токена). И +:

1     case '+':
2         {
3             const char tmp = *s++;
              ... code for ++ ...
4             if (PL_expect == XOPERATOR) {
                  ...
5                 Aop(OP_ADD);
6             }
              ...
7         }

Строка 4 проверяет, какой тип токена мы ожидаем. Aop возвращает токен. Если вы найдёте Aop в другом месте в toke.c, вы увидите, что он возвращает токен типа ADDOP.

Теперь, когда мы знаем два типа токенов, которые мы хотим найти в парсере, давайте возьмём фрагмент из perly.y, который нам нужен для построения дерева для $a = $b + $c

1 term    :   term ASSIGNOP term
2                { $$ = newASSIGNOP(OPf_STACKED, $1, $2, $3); }
3         |   term ADDOP term
4                { $$ = newBINOP($2, 0, scalar($1), scalar($3)); }

Если вы не знакомы с чтением грамматик Бэкуса-Наура, вот как это работает: токенизатор подаёт вам определённые вещи, которые обычно записываются заглавными буквами. ADDOP и ASSIGNOP являются примерами «терминальных символов», потому что их нельзя упростить.

Грамматика, строки одна и три фрагмента выше, показывает, как создавать более сложные формы. Эти сложные формы, «нетерминальные символы», обычно записываются строчными буквами. term здесь является нетерминальным символом, представляющим единственное выражение.

Грамматика даёт следующее правило: вы можете создать то, что слева от двоеточия, если увидите все вещи справа в последовательности. Это называется «приведением», и цель парсинга — полностью привести входные данные. Существует несколько различных способов выполнения приведения, разделённых вертикальными чертами: так, term за которым следует = за которым следует term образует term, а term за которым следует + за которым следует term также может образовать term.

Таким образом, если вы видите два термина с = или + между ними, вы можете превратить их в одно выражение. При этом вы выполняете код в блоке на следующей строке: если вы видите =, вы выполните код в строке 2. Если вы видите +, вы выполните код в строке 4. Именно этот код вносит вклад в дерево op.

|   term ADDOP term
{ $$ = newBINOP($2, 0, scalar($1), scalar($3)); }

Это создаёт новый бинарный op и передаёт ему несколько переменных. Переменные ссылаются на токены: $1 — первый токен на входе, $2 — второй, и так далее — представьте себе обратные ссылки регулярных выражений. $$ — op, возвращаемый при этом приведении. Итак, мы вызываем newBINOP для создания нового бинарного оператора. Первый параметр для newBINOP, функции из op.c, — это тип op. Это оператор сложения, поэтому нам нужен тип ADDOP. Мы могли бы указать это напрямую, но он прямо там, как второй токен на входе, поэтому мы используем $2. Второй параметр — флаги op: 0 означает «ничего особенного». Затем вещи для добавления: левая и правая части нашего выражения в скалярном контексте.

Функции, создающие op, которые имеют имена вроде newUNOP и newBINOP, вызывают функцию «проверки», связанную с каждым типом op, перед возвращением op. Функции проверки могут изменять op по своему усмотрению и даже заменить его полностью новым. Эти функции определены в op.c и имеют префикс Perl_ck_. Вы можете узнать, какая функция проверки используется для определенного типа op, посмотрев в regen/opcodes. Например, OP_ADD. (OP_ADD — значение токена из Aop(OP_ADD) в toke.c, которое парсер передаёт newBINOP в качестве своего первого аргумента.) Вот соответствующая строка:

add             addition (+)            ck_null         IfsT2   S S

Функция проверки в данном случае — Perl_ck_null, которая ничего не делает. Давайте рассмотрим более интересный случай:

readline        <HANDLE>                ck_readline     t%      F?

И вот функция из op.c:

 1 OP *
 2 Perl_ck_readline(pTHX_ OP *o)
 3 {
 4     PERL_ARGS_ASSERT_CK_READLINE;
 5 
 6     if (o->op_flags & OPf_KIDS) {
 7          OP *kid = cLISTOPo->op_first;
 8          if (kid->op_type == OP_RV2GV)
 9              kid->op_private |= OPpALLOW_FAKE;
10     }
11     else {
12         OP * const newop
13             = newUNOP(OP_READLINE, 0, newGVOP(OP_GV, 0,
14                                               PL_argvgv));
15         op_free(o);
16         return newop;
17     }
18     return o;
19 }

Одна из особенностей заключается в том, что если у op нет детей (т.е., readline() или <> ), op освобождается и заменяется совершенно новым, ссылающимся на *ARGV (строки 12-16).

СТЕКИ

Когда Perl выполняет что-то вроде addop, как он передаёт свои результаты следующему op? Ответ — с помощью стеков. Perl имеет несколько стеков для хранения вещей, над которыми он в данный момент работает, и здесь мы рассмотрим три наиболее важных.

Стек аргументов

Аргументы передаются коду PP и возвращаются из него с помощью стека аргументов, ST. Типичный способ обработки аргументов — извлечь их из стека, обработать их как нужно, а затем поместить результат обратно в стек. Вот как, например, работает оператор косинуса:

NV value;
value = POPn;
value = Perl_cos(value);
XPUSHn(value);

Мы увидим более сложный пример этого, когда рассмотрим макросы Perl ниже. POPn даёт вам NV (значение с плавающей точкой) верхнего SV в стеке: $x в cos($x). Затем мы вычисляем косинус и помещаем результат обратно как NV. X в XPUSHn означает, что стек должен быть расширен при необходимости — здесь это не нужно, потому что мы знаем, что есть место для ещё одного элемента в стеке, так как мы только что удалили один! Макросы XPUSH* как минимум гарантируют безопасность.

В качестве альтернативы, вы можете напрямую манипулировать стеком: SP даёт вам первый элемент вашей части стека, а TOP* даёт вам верхний SV/IV/NV и т. д. в стеке. Например, для выполнения унарного отрицания целого числа:

SETi(-TOPi);

Просто установите целое значение верхнего элемента стека в его отрицание.

Работа со стеком аргументов в ядре точно такая же, как и в XSUB. См. perlxstut, perlxs и perlguts для более подробного описания макросов, используемых в манипуляциях со стеком.

Стек меток

Я сказал «ваша часть стека» выше, потому что код PP не обязательно получает весь стек: если ваша функция вызывает другую функцию, вы захотите выставить только аргументы, предназначенные для вызываемой функции, а не (не обязательно) позволить ей получить доступ к вашим данным. Мы делаем это с помощью «виртуального» нижнего конца стека, доступного каждой функции. Стек меток сохраняет отметки о местоположениях в стеке аргументов, доступных каждой функции. Например, при работе со связанной переменной (внутренне, что-то с «магией» P) Perl должен вызывать методы для доступа к связанным переменным. Однако нам нужно разделить аргументы, доступные методу, от аргументов, доступных исходной функции — хранение, извлечение или что-то ещё. Вот примерно как реализован связанный push; см. av_push в av.c:

1  PUSHMARK(SP);
2  EXTEND(SP,2);
3  PUSHs(SvTIED_obj((SV*)av, mg));
4  PUSHs(val);
5  PUTBACK;
6  ENTER;
7  call_method("PUSH", G_SCALAR|G_DISCARD);
8  LEAVE;

Давайте рассмотрим всю реализацию для практики:

1  PUSHMARK(SP);

Поместите текущее состояние указателя стека в стек меток. Это делается для того, чтобы после завершения добавления элементов в стек аргументов Perl знал, сколько элементов мы добавили недавно.

2  EXTEND(SP,2);
3  PUSHs(SvTIED_obj((SV*)av, mg));
4  PUSHs(val);

Мы собираемся добавить ещё два элемента в стек аргументов: когда у вас есть связанный массив, подпрограмма PUSH получает объект и значение, которое должно быть помещено, и именно это у нас здесь — связанный объект, полученный с помощью SvTIED_obj, и значение, SV val.

5  PUTBACK;

Далее мы говорим Perl обновить глобальный указатель стека из нашей внутренней переменной: dSP дал нам только локальную копию, а не ссылку на глобальную.

6  ENTER;
7  call_method("PUSH", G_SCALAR|G_DISCARD);
8  LEAVE;

ENTER и LEAVE локализуют блок кода — они гарантируют, что все переменные обработаны надлежащим образом, всё, что было локализовано, возвращает своё предыдущее значение и так далее. Представьте их как { и } блока Perl.

Для фактического вызова магического метода нам нужно вызвать подпрограмму в пространстве Perl: call_method позаботится об этом, и оно описано в perlcall. Мы вызываем метод PUSH в скалярном контексте, и мы собираемся игнорировать его возвращаемое значение. Функция call_method() удаляет верхний элемент стека меток, поэтому вызывающий ничего не должен очищать.

Стек сохранения

В C нет понятия локального охвата, поэтому Perl предоставляет его. Мы видели, что ENTER и LEAVE используются как скобки охвата; стек сохранения реализует C-эквивалент, например:

{
    local $foo = 42;
    ...
}

См. "Локализация изменений" в perlguts для получения информации о работе со стеком сохранения.

Миллионы макросов

Вы заметите, что исходный код Perl содержит множество макросов. Некоторые назвали повсеместное использование макросов самым сложным для понимания, другие считают, что это добавляет ясности. Давайте рассмотрим пример, код, который реализует оператор сложения:

1  PP(pp_add)
2  {
3      dSP; dATARGET; tryAMAGICbin(add,opASSIGN);
4      {
5        dPOPTOPnnrl_ul;
6        SETn( left + right );
7        RETURN;
8      }
9  }

В каждой строке (кроме фигурных скобок, конечно) содержится макрос. Первая строка устанавливает объявление функции, как ожидается Perl для кода PP; строка 3 устанавливает объявления переменных для стека аргументов и целевой переменной, результата операции. Наконец, он пытается определить, перегружен ли оператор сложения; если да, вызывается соответствующая подпрограмма.

Строка 5 — это объявление ещё одной переменной — все объявления переменных начинаются с d — которая извлекает из вершины стека аргументов два NV (отсюда nn ) и помещает их в переменные right и left, отсюда и rl. Это два операнда оператора сложения. Далее мы вызываем SETn для установки NV возвращаемого значения в результат сложения двух значений. Сделав это, мы возвращаемся — макрос RETURN гарантирует, что наше возвращаемое значение обрабатывается должным образом, и мы передаём следующему оператору для выполнения обратно в главный цикл выполнения.

Большинство из этих макросов объяснены в perlapi, а некоторые из более важных — также в perlxs. Обратите особое внимание на "Обзор и PERL_IMPLICIT_CONTEXT" в perlguts для получения информации о макросах [pad]THX_?.

Дополнительные материалы

Для получения дополнительной информации о внутреннем устройстве Perl, см. документы по ссылке "Внутреннее устройство и интерфейс языка C" в perl.

© 1993–2020 Larry Wall and others
Licensed under the GNU General Public License version 1 or later, or the Artistic License.
The Perl logo is a trademark of the Perl Foundation.
https://perldoc.perl.org/5.28.3/perlinterp

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API