9

一篇名为“Perl 无法解析,正式证明”的文章正在流传。那么,Perl 是在“运行时”还是“编译时”决定其解析代码的含义?

在我读过的一些讨论中,我觉得这些论点源于不精确的术语,所以请尝试在你的答案中定义你的技术术语。我故意不定义“运行时”、“静态”或“解析”,以便我可以从可能与我不同定义这些术语的人那里获得观点。

编辑:

这与静态分析无关。这是一个关于 Perl 行为的理论问题。

4

5 回答 5

19

Perl 有一个明确定义的“编译时”阶段,然后是一个明确定义的“运行时”阶段。但是,有一些方法可以从一种过渡到另一种。许多动态语言具有eval允许在运行时编译新代码的结构。在 Perl 中,逆向也是可能的——而且很常见。块(以及由引起BEGIN的隐式块)编译时调用临时运行时阶段。块在编译后立即执行,而不是等待编译单元的其余部分(即当前文件或当前)编译。自从BEGINuseBEGINevalBEGINs 在后面的代码编译之前运行,它们几乎可以以任何方式影响后面代码的编译(尽管实际上它们所做的主要事情是导入或定义子例程,或启用严格性或警告)。

Ause Foo;基本上等同于BEGIN { require foo; foo->import(); }, require 是(就像eval STRING)从运行时调用编译时的一种方式,这意味着我们现在处于编译时内的运行时内的编译时内,并且整个事情是递归的。

无论如何,解析 Perl 的可判定性归结为,由于一段代码的编译会受到前面一段代码的执行的影响(理论上它可以做任何事情),我们已经得到了一个停止问题类型的情况;通常,正确解析给定 Perl 文件的唯一方法是执行它。

于 2009-08-14T23:31:08.990 回答
11

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在整个程序的其余部分中对调用的解释方式,可能会导致解析失败。)

好消息是不可能的情况非常极端。在技​​术上是可行的,但在实际代码中几乎可以肯定是无用的。因此,如果您正在编写静态分析工具,这可能不会给您带来麻烦。

公平地说,每一种称职的语言都有这个问题,或者类似的问题。举个例子,把你最喜欢的 Code walker 扔到这个 Lisp 代码中:

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

您可能无法预测这是一个产生列表的循环,因为iter宏是不透明的,需要特殊知识才能理解。现实情况是,这在理论上很烦人(如果不运行我的代码,或者至少运行iter宏,它可能永远不会停止运行这个输入,我无法理解我的代码),但在实践中非常有用(迭代对于程序员写作和未来的程序员阅读)。

最后,很多人认为 Perl 缺乏像 Java 那样的静态分析和重构工具,因为解析它相对困难。我怀疑这是真的,我只是认为没有必要,没有人费心去写它。(人们确实需要“lint”,例如 Perl::Critic。)

我需要对 Perl 进行的任何静态分析以生成代码(一些用于维护测试计数器和 Makefile.PL 的 emacs 宏)都运行良好。奇怪的极端案例会影响我的代码吗?当然,但我不会不遗余力地编写无法维护的代码,即使我可以。

于 2009-08-14T23:18:20.250 回答
5

人们已经用很多词来解释各个阶段,但这确实是一件简单的事情。在编译 Perl 源代码时,perl 解释器可能最终运行的代码会改变其余代码的解析方式。不运行代码的静态分析将错过这一点。

在 Perlmonks 的那篇文章中,Jeffrey 谈到了他在The Perl Review中的文章,这些文章更详细,包括一个示例程序,它每次运行时都不会以相同的方式解析。

于 2009-08-15T17:20:26.813 回答
3

C++ 在其模板系统中也存在类似的问题,但这并不能阻止编译器对其进行编译。他们只会在这种论点适用的极端情况下爆发或永远运行。

于 2009-08-14T23:27:42.743 回答
3

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 博客上有一些详细的帖子:

于 2009-08-14T23:28:54.847 回答