如何使用无上下文语法从标记列表构造抽象语法树?

时间:2015-07-26 06:50:53

标签: javascript parsing compiler-construction abstract-syntax-tree context-free-grammar

我在Javascript中编写C编译器,纯粹是为了丰富(到目前为止,我预计它没有实际用途,我可能不会维护它。)

我写了一个词法分析器,它可以成功地进行标记,给出正则表达式列表和正则表达式匹配的类型,任何字符串。

我已经能够成功地标记C源代码(稍微减少C,公平;我需要添加更多的标记化模式来捕获所有内容)。我现在想要将AST构建为源语言和已转换程序集之间的中间形式。

为此,我试图实现一个使用无上下文语法的函数,该语法被定义为具有

的对象
  • key→target(表达式,函数声明,强制转换表达式等)和
  • value→映射数组
    • (其中映射是构成目标的符号的数组[顺序很重要])。

以下是一个可能为解析器提供信息的示例CFG(这是this C grammar的改编摘录):

var cfg = {
    "cast_exp": [
        ["unary_exp"],
        ["(", "type_name", ")", "cast_exp"]
    ],   
    "unary_exp": [
        ["primary_exp"],
        ["++", "unary_exp"],
        ["--", "unary_exp"]
    ],
    "primary_exp": [
        ["id"]
    ]
};

id是我的tokenizer选择的类型之一,所以我想我们可以考虑" primary_exp"一个开始的符号。

现在,我的想法是递归地做这件事;也就是说,拿起第一个令牌并将其与其中一个起始符号匹配。递归剩余的令牌,发送我们在上一次调用中匹配的目标,并查看由我们刚匹配的目标组成的生产规则。

这对我和我看到的方式都没有多大意义,我会在无限递归中迷失(或者在很长的源文件上遇到堆栈溢出)。

如何编写一个可以遍历我的令牌数组的函数,并使用上面描述的CFG构建AST?因为我这样做是为了丰富和作为个人挑战,如果你喜欢你可以提供代码,但我更多的是寻求指导和对这种算法的广泛描述。

1 个答案:

答案 0 :(得分:4)

您可以实现Earley解析器。 (维基百科网站有代码, 所以我不提供任何东西。

这样的解析器在消耗令牌时从一个状态转换到另一个状态。在每个州,它都拥有“项目”的

 {  I1  I2 ...  In }

每个单独的项目Ik都是规则,并且已经处理了多少规则(称为“点”的地方)。

对于规则

  R = A B C D; 

在看到A和B的情况下,R的项目在概念上与带有点标记的规则相同:

  R = A B <dot> C D ; 

用点表示已经看到A和B,需要找到C。 状态/项集(可能)如下所示:

 {  P = Q <dot> R S ;I1
    R = A B <dot> C D ;
    C = <dot> X Y ;
 }

每个项目Ik代表了迄今为止看到的输入的可能解释;有多个项目的原因是输入可能有多个对输入流中的当前点有效的解释。处理令牌将改变状态/这组项目。

对于我们的示例规则R,当解析找到C 时(或者作为输入流中的标记,或者如果某个其他规则减少并将其左侧生成为非终结符) ,点被移动:

 R = A B C <dot> D;

为下一个解析器状态中的项目集创建新项目。

为每个令牌处理项目集中的所有规则;如果允许解析器“移动”下一个规则元素,则具有修改点的项目将处于下一个集合的状态;否则规则不再是输入的有效解释,并被丢弃(例如,不放在下一组中)。当移动点时,它表示可以进行新输入(对于上面的规则R,现在可以使用D),并且允许处理D的规则被添加到规则开头带有点的新状态。这可能会为集合添加几个新规则。

当一个点在远端结束时:

 R = A B C D <dot> ;

然后实际上R被视为非终结符(这被称为“减少”到R)并且可以用于在当前状态中提及R的其他规则中推进点:

 P = Q <dot> R S ;

转换为P = Q R S;

现在,当处理令牌时,此过程将应用于当前状态中的所有项目(规则+点)。

解析器以第一个状态启动,其中一个元素集由目标(你称之为“起始符号”)规则组成,带有一个点,表明该规则的任何部分都没有在你的情况下消耗:

{  primary = <dot> id ; }

有一点想法会让你得出结论,目标规则始终保留在项目集中的某个点。当目标规则中的点落在目标规则的末尾时,例如,当目标规则缩减为目标令牌时,解析就完成了,输入流完全被消耗。

Earley解析器相对较快,而且非常通用;他们将解析任何无上下文的语言。这令人惊讶的强大。 (如果您了解Earley解析器如何处理项目,您就可以了解大部分需要了解的内容,以了解LR解析器的工作原理)。它们很容易构建。

维基百科网站有一个更详细的例子。

对于使用Earley解析器(或任何类似类型的解析器)构建树,每当减少到R时,您可以构建一个树节点,其根是R类型,其子节点是先前为其元素构建的树。

显然,在处理终端令牌 t 时,可以为t构建单元树。 [很容易理解为什么当你意识到你的词法分析器实际上是一个子解析器时,它会将字符串“减少”到终端令牌。你可以将词法分析器规则放入在字符终端令牌上运行的语法中;出于效率原因,你只是选择不这样做。你可以这样做有趣; Earley解析器可以正常运行,但运行速度非常慢,因为现在它基于每个字符在更大的规则集上执行所有项目集管理。]。

在解析时跟踪所有这些东西看起来有点棘手,但实际上并不那么难。我留给读者。

对比,see how to do all this parsing and tree building using hand-coded recursive descent parsing。 (这些并不是那么强大,特别是它们可能很难用左递归语法规则,但如果你有语法,它们真的很容易写。)