虽然有几个关于如何使用Parser Combinator构建简单解析器的资源,但迄今为止还没有描述如何从头开始构建完整的词法和语法分析器。
下面将介绍如何构建词法分析器来将文本转化成一系列token,以及构建解析器将token解析为抽象语法树(AST)。
语言概述
“workflow code”用来描述工作流,即 可执行的有向无环图指令。以下是有效程序的示例。 请记住,块是通过缩进定界的,类似于Python语言。
|
|
我们希望将其解析成:
这个语法的BNF表示如下。
Parser combinators
parser combinator 是一个接受解析器作为输入并返回一个新的解析器作为输出的函数,类似于高阶函数。
举个例子,假设我们有一个parser int
来识别整数字面值,一个parserplus
来识别 ‘+’ 字符,我们可以产生一个解析器来识别 int plus int
序列为整数加法。
Scala标准库包括解析器组合器的实现:https://github.com/scala/scala-parser-combinators
构建词法分析程序
我们将会用到的标识符和字符串字面值的token,以及所有保留字和标点符号: exit
, read input
, call service
, switch
, otherwise
, :
, ->
, ==
, ,
。
我们还需要生成代表缩进增加和减少的人造token:INDENT
和DEDENT
。 现在请忽略这些,因为我们将在以后的阶段中进行讨论。
|
|
我们的词法分析器继承RegexParsers(一个Parsers的子类)。 RegexParsers
是专门用于使用正则表达式构建字符解析器的。 它提供了从String
和Regex
到Parser [String]
的隐式转换。
我们首先指定哪些字符应该被忽略为空格。 我们不能忽略\ n
,因为我们需要它来识别由它后面的空格数定义的标识级别。 每个其他空白字符都可以忽略。
|
|
现在我们构建一个标识符的解析器:
^^
操作符用来对解析结果进行映射。 正则表达式“[a-zA-Z _] [a-zA-Z0-9 _] *”.r
被隐式转换为Parser [String]的一个实例,映射是一个(String => IDENTIFIER)
函数,从而返回一个Parser[IDENTIFIER]
实例。
字符串字面值和标识的解析器类似:
关键字的解析器:
现在我们将把所有这些parser组合成一个能识别每一个token的解析器。 我们将利用以下操作符:
|
(or),识别任何token解析器;rep1
,识别他参数一次或多次重复phrase
,尝试消耗所有输入知道不再剩余
|
|
请注意,操作数的顺序在处理歧义时很重要。 如果我们identifier
放在其他token前面,我们的解析器将永远不会将它们识别为特殊关键字,因为它们将被成功地解析为标识符。
处理缩进
我们使用processIndentations
方法对我们的解析结果进行简短的后处理步骤。 用于从INDENTATION
token中生成人造INDENT
和DEDENT
token。缩进级别的每次增加都被压入栈中以产生INDENT
,缩进级别的缩小将从缩进堆栈弹出,生成DEDENT
。
现在所有的都设置好,这个token parser将把Reader[Char]
解析成ParseResult[List[WorkflowToken]]
。RegexParsers
定义了自己的Reader [Char]
,它由它提供的parser
方法内部调用。 然后我们为WorkflowLexer
定义一个apply
方法:
|
|
应用下样例:
构建语法分析器
现在我们已经进行了词法分析,我们仍然缺少语法分析步骤,即将token序列转换为抽象语法树(AST)。 与生成String
解析器的RegexParsers
不同,我们将需要一个WorkflowToken
parser。
|
|
我们需要定义Reader[WorkflowToken]
,被parser用来从WorkflowToken
序列中读取数据:
|
|
构建语法分析器和够贱词法分析器的过程类似。我们定义简单的解析器然后将他们组合成更复杂的解析器。只不过这次parser返回的是AST而不是token:
|
|
作为WorkflowToken
的parser我们继承了从WorkflowToken
到Parser[WorkflowToken]
的隐式转换。这对于解析无参token(如 EXIT
,CALLSERVICE
等)非常有用。对于INENTIFIER
和LITERAL
我们可以使用accept
方法对这些token进行匹配。
|
|
语法规则可以这样实现:
|
|
这与我们以前生成token类似; 这里我们将解析结果(identifier
,EQUALS
和literal
)转换成Equals
实例。注意~
的用法。
剩下的规则的实现和我们上面的类似:
就像我们用词法分析器一样,我们还定义了一个apply
方法:
Pipelining
我们现在已经实现了词法和语法分析器。 剩下的就是将它们串起来:
试一下样例:
我们的编译器已被证明能够解析代码。
错误处理
现在我们来尝试解析一个语法上无效的程序。 假设我们忘了在第一个switch case上引用PT
:
|
|
我们得到一个明确的错误消息,告诉我们预期一个字符串字面量,但在哪里? 知道这个错误的源位置就好了。 幸运的是,Scala的parser combinator支持记录token的原始源位置。
为了做到这一点,首先,我们的WorkflowToken
和WorkflowAST
trait 必须与Positional
混合。 这提供了一个可变的pos
变量和一个setPos
方法,该方法可以用行号和列号修饰对应的实例。
其次,我们必须在每个解析器上使用positioned
运算符。 例如,IDENTIFIER
token 的解析器将被写为:
|
|
Positional
Mixin的一个丑陋的副作用是,我们所有的令牌现在都必须成为case class 而不是case object,因为每个人现在都拥有可变状态。
我们的WorkflowCompilationError
子类型现在包括位置信息…
|
|
…由每个阶段的apply
方法报告:
现在我们再试一次解析无效的代码:
Final notes
我们从一个文本流分解成一系列token,最终将它们组合成一个类型化的抽象语法树,从而导致一个更有效的方法来推导程序。
我们现在可以扩展编译器以执行其他非解析相关任务,比如验证(例如,确保所有代码路径以exit
关键字结尾)或代码生成,即遍历此AST以生成指令序列。
如果您想继续使用Scala的parser combinator,则可以在以下网址获取本教程的最终代码:https://github.com/enear/parser-combinators-tutorial