Можно ли статически анализировать Perl?

статья под названием "Perl не может быть проанализирован, формальное доказательство" ходит по кругу. Итак, определяет ли Perl смысл своего проанализированного кода во время выполнения или во время компиляции?

В некоторых дискуссиях, которые я читал, у меня создалось впечатление, что аргументы проистекают из неточной терминологии, поэтому, пожалуйста, попробуйте определить свои технические термины в своем ответе. Я намеренно не определял термины «время выполнения», «статически» или «анализ», чтобы получить информацию от людей, которые, возможно, определяют эти термины иначе, чем я.

Редактировать:

Это не про статический анализ. Это теоретический вопрос о поведении Perl.


person Paul Biggar    schedule 14.08.2009    source источник
comment
comment
А теперь еще и modernperlbooks.com/ mt/2009/08/how-a-perl-5-program-works.html   -  person Robert P    schedule 18.08.2009
comment
Роберт П.: Модель выполнения Perl 5 определенно не совпадает с традиционным представлением об интерпретаторе. . Затем он продолжает описывать традиционного переводчика...   -  person Paul Biggar    schedule 18.08.2009
comment
Также из новостей Hacker: news.ycombinator.com/item?id=770072   -  person draegtun    schedule 18.08.2009
comment
@ Пол Биггар: Часть этого похожа на традиционный переводчик. Часть, где он прерывает выполнение до того, как закончит интерпретировать остальную часть кода, не выполняется.   -  person Robert P    schedule 19.08.2009
comment
@Robert P: Это не имеет ничего общего с переводом, это просто особенность языка. Вы также можете сказать, что это не традиционный интерпретатор, потому что он использует $_ (или выбирает функцию).   -  person Paul Biggar    schedule 19.08.2009


Ответы (5)


Perl имеет четко определенную фазу «времени компиляции», за которой следует четко определенная фаза «времени выполнения». Однако есть способы перехода от одного к другому. Многие динамические языки имеют eval конструкции, позволяющие компилировать новый код на этапе выполнения; в Perl возможно и обратное, и это распространено. Блоки BEGIN (и неявный блок BEGIN, вызванный блоком use) вызывают временную фазу выполнения во время компиляции. Блок BEGIN выполняется, как только он скомпилирован, вместо ожидания компиляции остальной части единицы компиляции (т. е. текущего файла или текущего eval). Поскольку BEGIN запускаются до компиляции кода, следующего за ними, они могут влиять на компиляцию следующего кода практически любым способом (хотя на практике основное, что они делают, — это импорт или определение подпрограмм или включение строгости или предупреждений).

use Foo; в основном эквивалентен BEGIN { require foo; foo->import(); }, а требование (как и eval STRING) является одним из способов вызвать время компиляции из среды выполнения, что означает, что теперь мы находимся внутри времени компиляции внутри времени выполнения внутри времени компиляции, и все это рекурсивно.

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

person hobbs    schedule 14.08.2009
comment
Чаще всего на компиляцию одного бита кода может повлиять компиляция предшествующего фрагмента кода, в частности, является ли идентификатор именем пакета или подпрограммы. - person ysth; 15.08.2009

В Perl есть блоки BEGIN, которые запускают пользовательский код Perl во время компиляции. Этот код может повлиять на смысл другого компилируемого кода, что делает «невозможным» разбор Perl.

Например, код:

sub foo { return "OH HAI" }

"действительно":

BEGIN {
    *{"${package}::foo"} = sub { return "OH HAI" };
}

Это означает, что кто-то может написать на Perl так:

BEGIN {
    print "Hi user, type the code for foo: ";
    my $code = <>;
    *{"${package}::foo"} = eval $code;
}

Очевидно, что ни один инструмент статического анализа не может угадать, какой код собирается ввести здесь пользователь. (И если пользователь скажет sub ($) {} вместо sub {}, это даже повлияет на то, как вызовы foo интерпретируются в остальной части программы, что может привести к сбою синтаксического анализа.)

Хорошая новость заключается в том, что невозможные случаи очень краеугольные; технически возможно, но почти наверняка бесполезно в реальном коде. Так что, если вы пишете инструмент статического анализа, это, вероятно, не доставит вам проблем.

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

(iter (for i from 1 to 10) (collect i))

Вы, вероятно, не можете предсказать, что это цикл, который создает список, потому что макрос iter непрозрачен, и для его понимания потребуются специальные знания. Реальность такова, что это раздражает в теории (я не могу понять свой код, не запустив его или, по крайней мере, не запустив макрос iter, который никогда не перестанет выполняться с этим вводом), но очень полезно на практике (итерация проста для программисту писать, а будущему программисту читать).

Наконец, многие люди думают, что в Perl отсутствуют инструменты статического анализа и рефакторинга, как в Java, из-за относительной сложности его разбора. Я сомневаюсь, что это правда, я просто думаю, что в этом нет нужды и никто не удосужился это написать. (Линт нужен, например, Perl::Critic.)

Любой статический анализ Perl, который мне нужно было выполнить для генерации кода (некоторые макросы emacs для поддержки счетчиков тестов и Makefile.PL), работал нормально. Могут ли странные угловые случаи сбить мой код? Конечно, но я не изо всех сил пишу код, который невозможно поддерживать, хотя мог бы.

person jrockway    schedule 14.08.2009
comment
Итак, почему вы используете термины «запускать код Perl во время компиляции», а не «компилировать код Perl во время выполнения». В чем отличие? Поэтому я и спросил о терминологии. - person Paul Biggar; 15.08.2009
comment
Значит, это просто терминология Perl-сообщества? Было бы так же правильно сказать, что вторая компиляция происходит во время выполнения блока BEGIN, как и сказать, что первое выполнение происходит во время фазы компиляции основного кода? - person Paul Biggar; 15.08.2009
comment
Да, хотя конец начальной фазы компиляции особенный. - person ysth; 15.08.2009
comment
Это не просто терминология. Хотя Perl может запускать некоторый код на этапе компиляции и, возможно, компилировать некоторый код на этапе выполнения, у каждого из них также есть обработчики для запуска в начале и в конце этапов. Хотя внутри они немного нечеткие, у них есть границы там, где происходят другие вещи. - person brian d foy; 16.08.2009
comment
@brian d foy: Да, но имена, данные этим фазам сообществом Perl, не отражают однозначно то, что эти фазы делают, и имена, выбранные для этих фаз, были бы такими же точными в противном случае. - person Paul Biggar; 19.08.2009
comment
@ Пол, нет, имена отражают большую задачу каждого из этих этапов. Названия целеустремленные, описательные и точные. - person brian d foy; 27.08.2009

Люди использовали много слов для объяснения различных фаз, но на самом деле это очень просто. При компиляции исходного кода Perl интерпретатор Perl может в конечном итоге запустить код, который изменяет способ синтаксического анализа остального кода. Статический анализ, который не запускает код, пропустит это.

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

person brian d foy    schedule 15.08.2009

C++ имеет аналогичную проблему в своей системе шаблонов, но это не мешает компиляторам компилировать его. Они просто вырвутся наружу или будут работать вечно в самых крайних случаях, где применим такой аргумент.

person Captain Segfault    schedule 14.08.2009
comment
Да, хорошо сказано. Та же идея, что и в моем посте, и намного меньше слов :) - person jrockway; 15.08.2009
comment
На самом деле это не похоже — для шаблонов C++ все задействованные значения также являются выражениями времени компиляции, и они явно отличаются от выражений времени выполнения. В Perl, в примере, приведенном в связанной статье, функция может быть определена по-разному, в зависимости, например, от пользователь вводит строку, поэтому остальная часть программы будет передаваться по-разному с момента ввода и далее. В C++ нет ничего даже отдаленно похожего. - person Pavel Minaev; 15.08.2009
comment
@Pavel Вы можете создать (почти) точный аналог примера в статье на C++, используя шаблоны и неоднозначность объявления/инициализации. Тот факт, что Perl может передать это во время выполнения, тогда как компилятор C++ должен разрешить это во время компиляции, не имеет значения. - person Captain Segfault; 15.08.2009
comment
@Segfault: статический анализ выполняется перед выполнением. - person Paul Nathan; 18.08.2009

В Perl есть фаза компиляции, но она отличается от большинства обычных фаз компиляции, когда речь идет о коде. Лексер Perl превращает код в токены, затем синтаксический анализатор анализирует токены и формирует дерево операций. Однако блоки BEGIN {} могут прервать этот процесс и позволить вам выполнить код. При выполнении use. Все блоки BEGIN выполняются раньше всего, что дает вам возможность устанавливать модули и пространства имен. Во время общей «компиляции» сценария вы, скорее всего, будете использовать Perl, чтобы определить, как должен выглядеть модуль Perl после его завершения. sub, bare, подразумевает добавление его в glob для пакета, но это не обязательно. Например, это (хоть и странный) способ настройки методов в модуле:

package Foo;

use strict;
use warnings;
use List::Util qw/shuffle/;

my @names = qw(foo bar baz bill barn);
my @subs = (
    sub { print "baz!" },
    sub { die; },
    sub { return sub { die } },
);
@names = shuffle @names;
foreach my $index (0..$#subs) {
   no strict 'refs';
   *{$names[$index]} = $subs[$index];
}

1;

Вы должны интерпретировать это, чтобы даже знать, что оно делает! Это не очень полезно, но это не то, что вы можете определить заранее. Но это 100% правильный perl. Несмотря на то, что этой функцией можно злоупотреблять, она также может выполнять отличные задачи, например, создавать сложные сабвуферы, которые программно выглядят очень похожими. Кроме того, трудно точно знать, что все делает.

Это не значит, что Perl-скрипт нельзя «скомпилировать» — в Perl компиляция просто определяет, как в данный момент должен выглядеть модуль. Вы можете сделать это с помощью

perl -c myscript.pl

и он сообщит вам, сможет ли он добраться до точки, где он начнет выполнение основного модуля. Вы просто не можете просто знать, глядя на это «статически».

Однако, как показывает PPI, мы можем приблизиться к этому. Очень близко. Достаточно близко, чтобы делать очень интересные вещи, такие как (почти статический) анализ кода.

Таким образом, «время выполнения» становится тем, что происходит после выполнения всех BEGIN блоков. (Это упрощение; это гораздо больше. См. perlmod для получения дополнительной информации.) Это по-прежнему выполняется код perl, но это отдельная фаза выполнения, выполняемая после выполнения всех блоков с более высоким приоритетом.

chromatic опубликовал несколько подробных статей в своем блоге Modern::Perl:

person Robert P    schedule 14.08.2009
comment
Предположительно, вы можете заставить блок BEGIN проверять что-то в файловой системе или сети, в результате чего получится два разбора одной и той же программы с двумя разными значениями? - person Paul Biggar; 15.08.2009
comment
Абсолютно. Я видел (возможно, заблуждающихся), как Perl-разработчики используют блок BEGIN для разбора аргументов в командной строке, а затем изменяют переменные, доступные для выполнения, на основе этого. На самом деле вам даже не нужно делать это на этапе компиляции; приведенный выше код может выполняться несколько раз. Вы можете использовать его в функции и даже изменить поведение модуля после его компиляции. Вредоносность кода Perl — это то, с чем на самом деле могут соперничать только другие динамические языки; В качестве яркого примера на ум приходят языки, подобные LISP. - person Robert P; 15.08.2009
comment
Хорошо, но это просто игра с таблицей символов. Но не может же строка изменить значение во время программы (в смысле цитируемой статьи), не так ли? - person Paul Biggar; 15.08.2009
comment
@Paul Biggar: В статье речь идет не о дереве синтаксического анализа для небольшого изменения кода во время выполнения, а о невозможности (в общем случае) определить дерево синтаксического анализа для этого кода без выполнения чего-либо. - person Michael Carman; 15.08.2009