一篇名为“Perl 无法解析,正式证明”的文章正在流传。那么,Perl 是在“运行时”还是“编译时”决定其解析代码的含义?
在我读过的一些讨论中,我觉得这些论点源于不精确的术语,所以请尝试在你的答案中定义你的技术术语。我故意不定义“运行时”、“静态”或“解析”,以便我可以从可能与我不同定义这些术语的人那里获得观点。
编辑:
这与静态分析无关。这是一个关于 Perl 行为的理论问题。
一篇名为“Perl 无法解析,正式证明”的文章正在流传。那么,Perl 是在“运行时”还是“编译时”决定其解析代码的含义?
在我读过的一些讨论中,我觉得这些论点源于不精确的术语,所以请尝试在你的答案中定义你的技术术语。我故意不定义“运行时”、“静态”或“解析”,以便我可以从可能与我不同定义这些术语的人那里获得观点。
这与静态分析无关。这是一个关于 Perl 行为的理论问题。
Perl 有一个明确定义的“编译时”阶段,然后是一个明确定义的“运行时”阶段。但是,有一些方法可以从一种过渡到另一种。许多动态语言具有eval
允许在运行时编译新代码的结构。在 Perl 中,逆向也是可能的——而且很常见。块(以及由引起BEGIN
的隐式块)在编译时调用临时运行时阶段。块在编译后立即执行,而不是等待编译单元的其余部分(即当前文件或当前)编译。自从BEGIN
use
BEGIN
eval
BEGIN
s 在后面的代码编译之前运行,它们几乎可以以任何方式影响后面代码的编译(尽管实际上它们所做的主要事情是导入或定义子例程,或启用严格性或警告)。
Ause Foo;
基本上等同于BEGIN { require foo; foo->import(); }
, require 是(就像eval STRING
)从运行时调用编译时的一种方式,这意味着我们现在处于编译时内的运行时内的编译时内,并且整个事情是递归的。
无论如何,解析 Perl 的可判定性归结为,由于一段代码的编译会受到前面一段代码的执行的影响(理论上它可以做任何事情),我们已经得到了一个停止问题类型的情况;通常,正确解析给定 Perl 文件的唯一方法是执行它。
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 宏)都运行良好。奇怪的极端案例会影响我的代码吗?当然,但我不会不遗余力地编写无法维护的代码,即使我可以。
人们已经用很多词来解释各个阶段,但这确实是一件简单的事情。在编译 Perl 源代码时,perl 解释器可能最终运行的代码会改变其余代码的解析方式。不运行代码的静态分析将错过这一点。
在 Perlmonks 的那篇文章中,Jeffrey 谈到了他在The Perl Review中的文章,这些文章更详细,包括一个示例程序,它每次运行时都不会以相同的方式解析。
C++ 在其模板系统中也存在类似的问题,但这并不能阻止编译器对其进行编译。他们只会在这种论点适用的极端情况下爆发或永远运行。
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 博客上有一些详细的帖子: