2011-04-29 93 views
25

我目前正在构建用PHP编写的PHP解析器,因为没有现有解析器出现在my previous question中。 parser itself工作得很好。将AST编译回源代码

现在很明显,解析器本身并没有什么好处(除了静态分析)。我想将转换应用于AST,然后将其编译回源代码。应用转换不是一个问题,一个正常的访问者模式应该做。

我现在的问题是如何编译AST回源。基本上有两种可能性,我看到:

  1. 编译使用一些预定义的方案
  2. 保留原始代码的格式,并只对已更改节点申请1中的代码。

现在我想专注于1.作为2.似乎很难完成(但如果你有关于这方面的提示,我想听听他们)。

但我不确定可以使用哪种设计模式来编译代码。我看到实现这个最简单的方法是将->compile方法添加到所有节点。我在这里看到的缺点是要更改生成的输出的格式非常困难。人们需要改变节点本身才能做到这一点。因此我正在寻找不同的解决方案。

我听说Visitor模式也可以用于此,但我无法真正想象这应该如何工作。据我了解访问者模式,你有一些NodeTraverser递归遍历所有节点,并调用一个->visit方法Visitor。这听起来很有希望用于节点操作,其中Visitor->visit方法可以简单地更改它传递的节点,但我不知道它如何用于编译。一个显而易见的想法是将节点树从叶子迭代到根,并用源代码替换访问的节点。但这似乎不是一个非常干净的解决方案?

+0

我想你给了我50分。恩,谢谢! – 2011-10-06 21:20:05

回答

51

将AST转换回源代码的问题通常称为“漂亮打印”。有两个微妙的变化:尽可能重新生成与原始文本匹配的文本(我称之为“保真度打印”)和(漂亮)漂亮打印,这会生成很好的格式化文本。以及如何根据编码人员是否将在重新生成的代码上工作(他们通常希望保真度打印)来打印问题 ,或者您唯一的意图是编译它(此时任何合法的漂亮打印都可以)。

要做好漂亮的打印需要的信息通常比经典的解析器收集的信息要多,而且大多数解析器生成器都不支持这种额外的信息收集。我打电话给解析器,收集足够的信息来做到这一点,“重新设计解析器”。以下更多细节。

漂亮打印的基本方法是通过行走AST(“放置它”时的“访问者模式”),并根据AST节点内容生成文本。基本技巧是:从左到右调用子节点(假定这是原始文本的顺序)以生成它们表示的文本,并为此AST节点类型散布其他文本。要打印一段语句,您可能需要以下伪代码:

PrettyPrintBlock: 
    Print("{"}; PrintNewline(); 
    Call PrettyPrint(Node.children[1]); // prints out statements in block 
    Print("}"); PrintNewline(); 
    return; 


PrettyPrintStatements: 
    do i=1,number_of_children 
     Call PrettyPrint(Node.children[i]); Print(";"); PrintNewline(); // print one statement 
    endo 
    return; 

请注意,当您访问树时,会随即弹出文本。

有一些细节你需要管理:

  • 对于代表文字AST节点,你必须重新生成文本值。如果你想让答案准确,这比看起来更难。在不损失任何精度的情况下打印浮点数是lot比看起来更难(当你损害Pi的值时,科学家讨厌它)。对于字符串文字,您必须重新生成引号和字符串文字内容;您必须小心重新生成必须转义的字符的转义序列。 PHP双引号字符串文字可能有点困难,因为它们不是由AST中的单个标记表示的。 (我们的PHP Front End(重新编译解析器/ prettyprinter将它们实质上表示为连接字符串片段的表达式,使得变换可以在字符串“literal”内应用)

  • 间距:某些语言在关键位置需要空格。令牌ABC17 42最好不要打印为ABC1742,但可以将令牌(ABC17)打印为(ABC17)。解决此问题的一种方法是在合法的位置放置一个空间,但人们不会喜欢结果是:太多的空格,如果你只是编译结果,不是问题

  • 换行符:允许任意空格的语言在技术上可以重新生成一行文本。你要编译结果;有时你看看生成的代码,这使得它不可能。因此,您需要一种方法来为表示主要语言元素(语句,块,方法,类等)的AST节点引入换行符。这通常不困难;当访问代表这样一个构造的节点时,打印构造并追加一个换行符。

  • 如果您希望用户接受您的结果,您将会发现,您将不得不保留源文本的某些属性,而这些属性通常不会存储为 对于文字,您可能必须重新生成基数的字面意思;当您重新生成十进制等值时,即使它意味着完全相同的东西,输入数字作为十六进制文字的编码器也不会很高兴。同样,字符串必须有“原始”引号;大多数语言都允许使用“或”作为字符串引号字符,并且人们需要它们最初使用的字符。对于PHP,使用哪些引用来说明问题,并确定字符串字符串中的哪些字符必须转义 有些语言允许使用大写或小写关键字(甚至是缩写),大写和小写变量名称表示同一个变量;原始作者通常希望他们的原始封套返回.PHP具有不同类型标识符中的有趣字符(例如“$”),但是您会发现它并不总是存在的(参见字符串中的$变量)通常人们都希望他们的原始布局格式化;为此,您必须存储具体令牌的列号信息,并且有关何时使用该列的明确规则 - 数据数据,用于在可能的情况下将相同列中的相应打印文本放在哪个位置;如果在该列之后填充了那么深的打印行,该如何处理?

  • 评论:大多数标准解析器(包括您使用Zend解析器实现的解析器,我敢肯定)完全抛弃注释。再次,人们讨厌这个问题,并且会拒绝一个评论丢失的印刷精美的答案。这是一些美丽的打印机试图通过使用原始文本 重新生成代码的主要原因(另一种是复制原始代码布局以进行保真度打印,如果您未捕获到列号信息)。恕我直言,正确的诀窍是捕获AST中的评论,以便AST转换可以检查/生成评论,但每个人都有自己的设计选择。

所有这些“额外”信息都是由一个好的重新构造解析器收集的。传统的解析器通常不收集任何信息,这使打印可接受的AST变得困难。

一个更原则的方法区分其打印目的是漂亮的格式,从保真度打印,其目的是重新生成文本,以最大限度地匹配原始来源。应该清楚的是,在终端级别,你几乎要保真度打印。根据您的目的,您可以使用漂亮的格式或高保真打印进行漂亮的打印。我们使用的策略是当AST没有改变时默认打印保真度打印,并且打印它的位置(因为变更机器通常没有关于列号或数字基数的信息等)。转换会将新生成的AST节点标记为“不存在保真度数据”。

一个有组织的方法,很好地打印漂亮的是要理解几乎所有基于文本的编程语言在矩形块文本方面很好地呈现。 (Knuth的TeX文档生成器也有这个想法)。如果您有一些文本框代表重新生成的代码片段(例如,直接为终端令牌生成的原始框),您可以想象操作员编写这些框:水平合成(在另一个框右侧堆叠一个框),垂直(相互叠放盒,这实际上取代了印刷新行),缩进(用空格一盒水平线构图),等等,那么你可以通过建立和撰写文本框中构建您的prettyprinter:

PrettyPrintBlock: 
    Box1=PrimitiveBox("{"); Box2=PrimitiveBox("}"); 
    ChildBox=PrettyPrint(Node.children[1]); // gets box for statements in block 
    ResultBox=VerticalBox(Box1,Indent(3,ChildBox),Box2); 
    return ResultBox; 

PrettyPrintStatements: 
    ResultBox=EmptyBox(); 
    do i=1,number_of_children 
     ResultBox=VerticalBox(ResultBox,HorizontalBox(PrettyPrint(Node.children[i]); PrimitiveBox(";") 
    endo 
    return; 

这里的真正价值是任何节点都可以用任意的顺序用任意的中间文本来组合由其子节点生成的文本框。您可以用这种方法重新排列大量文本块(想象一下VBox在方法名顺序中的类方法)。没有文字被吐出来遇到;只有到达根目录时,或者某个AST节点已知所有子项框已正确生成。

我们的DMS Software Reengineering Toolkit使用这种方法来清理它可以解析的所有语言(包括PHP,Java,C#等)。相反,通过游客附着框计算到AST节点,我们重视箱计算在特定领域的文本框标记

  • H(...)卧式箱
  • V(.... )对于纵向盒
  • 我(...)为缩进框)

直接到语法规则,使我们能够简洁地表达语法(解析器)和prettyprinter(“反解析”)一个地方。漂亮的打印机规则由DMS自动编译到访问者中。漂亮的打印机必须足够聪明才能理解评论如何发挥到这一点,坦白说这有点神秘,但你只需要做一次。一个DMS例如:

block = '{' statements '}' ; -- grammar rule to recognize block of statements 
<<PrettyPrinter>>: { V('{',I(statements),'}'); }; 

你可以看到这是如何了Wirth's Oberon programming language PrettyPrinter展示如何将语法规则和漂亮的方式规则组合做了更为明显的例证。 PHP前端看起来像这样,但它显然很大。

执行漂亮打印的更复杂的方法是构建一个语法导向的转换器(意思是,沿着树形结构顺序遍历树并构建文本或其他数据结构),以在特殊文本框中生成文本框AST。文本框AST然后被另一棵树行走打印,但其行为基本上是微不足道的:打印文本框。 看到这个技术文件:Pretty-printing for software reengineering

另外一点:你当然可以自己建立所有这些机械。但是,你选择使用解析器生成器(它做了很多工作,并且这个工作并没有以有趣的方式对你的目标做出贡献)的原因与你想要选择一个现成的解析器生成器的原因是一样的,货架漂亮打印机。周围有很多解析器生成器。没有很多漂亮的打印机。 [DMS是为数不多的内置之一。]

+0

感谢您的广泛描述,我会在接下来的几天尝试这些提示。 PS:Simlpe语言例子链接给了我404。 – NikiC 2011-04-29 17:05:47

+0

@nikic:在我第一次提交时是错误的,但我纠正了它。再试一次。 – 2011-04-29 17:09:20

+1

好的,谢谢你的帮助Ira :)我设法实现了漂亮的打印机(花了相当多的时间来摆脱许多边缘案例的bug)。虽然它不保留任何空白或评论信息。我认为这很难实施。你可以在github上找到结果包:https://github.com/nikic/PHP-Parser :)再次感谢! – NikiC 2011-06-04 14:00:05