用 Python 从零开始写一个简单的解释器(2)

在《用 Python 从零开始写一个简单的解释器(1)》中,我们介绍了 IMP 语言以及我们为它打造的解释器的通用结构。也深入介绍了词法分析器。本文中,我们准备写一个小型的解析器组合子(parser combinator)的库。这个库将被用来创建 IMP 语法分析器,语法分析器的作用是从由词法分析器生成的标记符列表中提取一个抽象语法树(AST)。该解析器组合子的库是语言无关的,所以你可以用它来为任意语言写语法分析器。

什么是解析器组合子?

构建一个语法分析器/解析器有许多许多的方法。而组合子也许是编写一个能启动并运行的解析器的最简单、最快速的方法了。

你可以将一个解析器想象成一个函数。它接收一个标记符流作为输入。如果成功的话,语法分析器会「消耗」标记符流中的一部分标记符。它将返回最终抽象语法树(AST)的一部分,以及剩下的标记符。一个组合子本身也是一个函数,它生成解析器作为输出,一般情况下,它需要以一个或多个解析器作为输入,因此得名“组合子”。你可以先为语言的某些部分创建许多小的解析器,再用组合子来构建最终的解析器。 通过这种方式,你便能使用组合子来创建一个类似 IMP 的完整语言。

一个最小的组合子库

解析器组合子相对来说较为通用,且能用在任意语言上。就像我们在编写词法分析器时所做的一样,我们会先写一个语言无关的组合子库,之后用它来完成我们的 IMP 语法分析器。

首先,让我们定一个 Result 类。每个解析器在解析成功时都会返回一个 Result 对象,错误时则返回 None 。一个 Result 对象包括了一个值(作为 AST 的一部分)以及一个位置信息(标记符流中 一下个标记符的索引)。

接着,我们将定义一个基类 Parser 。先前,我提到解析器本质上是以标记符流为输入的函数。实际上我们也把解析器定义成带有 call 方法的对象。这意味着一个语法分析器对象会表现得函数一样,但我们也可以通过定义其它的一些操作符来提供额外的功能。

实际执行解析的方法是 call 。它的输入是完整的标记符列表(由词法分析器返回)以及指向列表中的下一个标记符的索引。call 方法的默认实现将总是返回 None(即解析失败)。Parser 的子类将提供它们自己的 call 方法的实现。

其它的方法, addmulor、及 xor 分别定义了 + 、 * 、 | 、及 ^ 操 作符。每个操作符提供了调用不同组合子的快捷方法。我们不久就要介绍到它们。

接下来,我们来看看最简单的组合子,Reserved 。

Reserved 将被用来解析保留关键字及操作符;它将接受有特定值和标签的标记符。

请记住,标记符只是值-标签对。token[0] 代表值,token[1] 代表标签。

At this point, you might stop and say, “I thought combinators were going to be functions returning parsers. This subclass doesn’t look like a function.”

现在呢,你可能会停下说,“我还以为组合了会是返回解析器的函数。可这个子类并不像是函数啊”。如果你把组合子的构造函数当成是一个返回对象(在当前情况下它正好也是可调用的)的函数的话, 它组合子本身也就像是函数一样了。用子类化来定义新的组合子很简单,因为我们只需要提供一个构造函数和一个 call 方法,同时我们也还能获得其它的功能(比如那些重载的运算符)。

我们继续,Tag 组合子和 Reserved 十分相似。它匹配有某一特殊标签的任意标记符。标记符的值可以是任意值。

Concat 组合子需要两个解析器作为输入(左输入和右输入)。一个 Concat 解析器在被调用的时候,会先调用左解析器,再调用右解析器。如果两个解析器都解析成功了,则返回一个包含了左右解析器返回结果的对。如果有一个解析器解析不成功,则返回 None 。

Concat 可用于解析一个特定的标记符序列。例如,要解析 1+2 ,你可以写

或着用 + 运算符表示,更为简洁:

Alternate 组合子也类似,它也需要两个参数:左解析器和右解析器。它先调用左解析器,如果解析成功了,刚返回相应的结果。如果不成功,则调用右解析器并返回它的结果。

Alternate 可用于在几个可能的解析器中进行选择。例如,如果我们想解析任意的二元运算符:

Opt 可用于解析可选的文本,例如 if 语句中的 else 子句。它需要一个语法分析器作为输入,如果该解析器调用成功,则正常返回它的结果。如果失败,仍返回一个成功的结果,但该结果的值为 None。调用失败时,解析器不消耗标记符,结果的位置与输入的位置相同。

Rep 组合子将重复调用作为输入的解析器,直到失败为止。它可以用来生成某样事物的列表。要注意的是,如果解析器第一次调用就失败了, Rep 仍旧成功返回,此时它匹配的是一 个空的列表,并且不消耗标记符。

Process 是一个有用的组合子,可以用来处理结果的值。它的输入是一个解析器和一个函数。当解析器被成功调用时,Process 会将它的结果传给作为输入的函数作为参数,并用该函数返回的结果取代原本的值作为返回的结果。我们会使用 Process 来将 Concat 及 Rep 返回的结果对及结果列表实际构建成 AST 节点。举个例子,假设我们使用 Concat 构建了一个语法分析器,当它解析 1+2 时,真正返回的结果为 ((‘1’, ‘+’), ‘2’) ,这个结果并不十分有用。使用 Process 我们可以修改返回的结果,例如,下面的解析器就能累加解析得到的表 达式。

Lazy 也是一个很有用的组合子,尽管不那么明显。其它组合子需要解析器作为输入,与之不同,Lazy 接受一个函数作为参数,该函数接收零个参数并返回一个解析器。除非被调用了,否则 Lazy 本身不会调用这个函数来获取解析器。要构建递归解析器(就如算术表达式本身可以包含另一个算术表达式)的话就得用到它。这是由于递归解析器会调用自己,我们并不能直接在定义时就直接调用自己;因为在该解析器定义语句运行时,解析器本身还没有被完整定义。使用 Haskell 或 Scala 等支持惰性运算的语言时我们并不需要它,但无奈 Python 什么支持,就是不支持惰性运算。

下一个组合子 Phrase 接受一个单独的解析器作为输入,调用它并正常地返回它的结果。唯一的不同是如果它的解析器没有消耗所有剩余的标记符,则 Phrase 调用失败。IMP 语言的最高层将会是一个 Phrase 解析器。这会防止我们匹配一个末尾充满无用内容的程序。

很不幸,最后一个组合子也是最复杂的一个。Exp 的使用场合比较特殊;它将用来匹配一个表达式,该表达式包含一些元素,这些元素以某些内容分隔。复合语句就是其中的一个例子。

这个例子中包含了由分号隔开的一系列语句。你可能觉得我们并不需要 Exp,因为我们可以用其它的组合子来完成相同的功能。我们可以直接为复合语句写一个像这样的解析器:

可你得想想我们要如何定义 stmt。

这样的话 stmt 一执行就调用 compound_stmt ,而它一开始执行又调用了 stmt 。这两个解析器会不停地相互调用,直至栈溢出为止。这个问题不局限于复合语句。算术表达式和布尔表达式也存在同样的问题(考虑一下像 + 等等的运算符或把 and 当成分隔符)这个问题被称为左递归,解析器组合子无法很好地解决。

幸运地是,Exp 为我们提供了左递归的解决方法,即匹配一个列表,就像 Rep 一样。Exp 以两个解析器作为输入。第一个解析器用于匹配列表中真正的元素。第二个解析器用于匹配分隔符。成功时,分隔符解析器需要返回一个函数,来将解析得到的左右结果合并成一个单独的 值。这个结果是对整个列表的累加,从左向右,最终作为结果返回。

让我们看看实际的代码:

result 永远包含当前解析得到的所有信息。process_next 是一个函数, Process 会用到。next_parser 会首先调用 separator,接着调用 parser 来得到列表中的下一个元素。process_next 会以当前结果和新解析的元素作为参数,调用 separator 函数来创建 一个新的结果。next_parser 不断地被循环调用,直到无法匹配更多的元素。

让我们看看 Exp 如何解决我们的 compound_stmt 问题。

我们也可以写成这样:

下一篇文章中,我们会介绍如何解析算术表达式,届时我们会介绍更多的细节。

未完待续

本文中,我们会创建一个最小的解析器组合子库。这个库可以用来为几乎任何的计算机语言编写语法分析器。

下篇文章中,我们会为 IMP 创建抽象语法树需要的数据结构,并且我们会用本文中的库来定义一个语法分析器。

如果你对现在就想试试 IMP 解释器,或者你想查看所有的源代码,那么现在就可以来下载它吧。

打赏支持我翻译更多好文章,谢谢!

打赏译者

打赏支持我翻译更多好文章,谢谢!

任选一种支付方式

1 3 收藏 2 评论

关于作者:LotAbout

简介还没来得及写 :) 个人主页 · 我的文章 · 26 ·    

相关文章

可能感兴趣的话题



直接登录
最新评论
  • jaggie   2015/10/19

    原文里面 Process 类的代码忘记贴上去了


    class Process(Parser):
    def __init__(self, parser, function):
    self.parser = parser
    self.function = function

    def __call__(self, tokens, pos):
    result = self.parser(tokens, pos)
    if result:
    result.value = self.function(result.value)
    return result

跳到底部
返回顶部