【问题标题】:Can Perl be "statically" parsed?Perl 可以“静态”解析吗?
【发布时间】:2010-11-19 19:37:17
【问题描述】:

article called "Perl cannot be parsed, a formal proof" 正在巡视。那么,Perl 是在“运行时”还是“编译时”决定其解析代码的含义?

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

编辑:

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

【问题讨论】:

标签: perl parsing runtime interpreter dynamic-languages


【解决方案1】:

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

use Foo; 基本上等同于 BEGIN { require foo; foo->import(); },其中 require 是(如 eval STRING)从运行时调用编译时的方法之一,这意味着我们现在在编译时在运行时内-time,整个事情都是递归的。

无论如何,解析 Perl 的可判定性归根结底是因为一段代码的编译可能会受到前一段代码的执行的影响(理论上可以做任何事情),我们遇到了一个停顿问题类型的情况; 一般来说正确解析给定 Perl 文件的唯一方法是执行它。

【讨论】:

  • 更多时候,一段代码的编译会受到前面一段代码的编译的影响——尤其是标识符是包名还是子名。
【解决方案2】:

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 代码”。有什么区别?这就是我问术语的原因。
  • 所以它只是来自 Perl 社区的术语?我们可以说第二次“编译”发生在 BEGIN 块的执行期间,就像说第一次执行发生在主代码的编译阶段一样正确?
  • 是的,尽管初始编译阶段的结束是特殊的。
  • 这不仅仅是术语。尽管 Perl 可能会在编译阶段运行一些代码,也可能在运行阶段编译一些代码,但它们中的每一个都有在阶段开始和结束时运行的钩子。虽然它们的内部有点模糊,但它们在发生其他事情时有界限。
  • @Paul,不,这些名称反映了每个阶段的大任务。这些名称是有目的的、描述性的和准确的。
【解决方案3】:

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

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

【讨论】:

    【解决方案4】:

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

    【讨论】:

    • 是的,说得好。与我的帖子相同的想法,并且单词更少:)
    • 这不是很相似——对于 C++ 模板,所有涉及的值也是编译时表达式,它们明显不同于运行时表达式。在 Perl 中,通过给出链接文章的示例,该函数可以根据例如不同的方式定义。字符串用户输入,因此程序的其余部分将从输入的那一刻起以不同的方式传递。 C++ 中甚至没有任何相似之处。
    • @Pavel 您可以(几乎)使用模板和声明/初始化歧义在 C++ 中创建(几乎)文章中的示例的完全类似物。 Perl 可以将它放到运行时,而 C++ 编译器需要在编译时解析它这一事实是无关紧要的。
    • @Segfault:静态分析发生在运行前。
    【解决方案5】:

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

    【讨论】:

    • 想必你可以让BEGIN块检查文件系统或网络上的某些东西,从而导致对相同程序的两次解析具有两种不同的含义?
    • 当然。我已经看到(也许是被误导的)perl 开发人员使用 BEGIN 块来解析命令行上的参数,然后根据它更改可用于执行的变量。实际上,您甚至不必在“编译”阶段执行此操作;上面的代码可以执行多次。您可以将它放在一个函数中,甚至可以在“编译”后更改模块的行为。 perl 代码的易变性实际上只有其他动态语言才能与之匹敌。类似 LISP 的语言就是一个很好的例子。
    • 好的,但这只是在玩符号表。但是在程序期间一行不能改变意思(在引用文章的意义上),不是吗?
    • @Paul Biggar:这篇文章不是在讨论在运行时更改一些代码的解析树,而是在讨论无法(在一般情况下)为此确定解析树代码不执行任何操作。
    猜你喜欢
    • 2013-05-01
    • 2018-09-30
    • 1970-01-01
    • 2017-04-02
    • 1970-01-01
    • 1970-01-01
    • 2011-10-11
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多