基本技巧是要认识到解析,无论多么完成,都是在增量步骤中发生的,包括一个一个地读取标记。
在每个增量步骤中,都有机会通过组合其他增量步骤构建的 AST 片段来构建 AST 的一部分。这是一个递归的想法,它会在扫描令牌时为令牌构建 AST 叶节点。这个基本思想出现在几乎所有的 AST 构建解析器中。
如果一个人构建了一个递归下降解析器,那么一个人实际上构建了一个递归过程的协作系统,每个递归过程都可以识别正在实施的任何语法中的非终结符。对于纯解析,每个过程只返回一个布尔值,表示“非终结(未)识别”。
要使用递归下降解析器构建 AST,需要将这些过程设计为返回两个值:布尔值“已识别”,以及(如果已识别)为非终结符构造(以某种方式)的 AST。 (一个常见的技巧是返回一个指针,它对于“未识别”是无效的,或者如果“识别”则指向构造的 AST)。构建单个过程的结果 AST 的方式是组合来自它调用的子过程的 AST。这对于读取输入标记并可以立即构建树的叶过程来说非常简单。
所有这一切的缺点是必须手动编码递归下降,并通过树构建步骤对其进行扩充。总体而言,这实际上很容易为小语法编写代码。
对于 OP 的例子,假设我们有这样的语法:
GOAL = ASSIGNMENT
ASSIGNMENT = LHS '=' RHS ';'
LHS = IDENTIFIER
RHS = IDENTIFIER | NUMBER
好的,我们的递归下降解析器:
boolean parse_Goal()
{ if parse_Assignement()
then return true
else return false
}
boolean parse_Assignment()
{ if not Parse_LHS()
then return false
if not Parse_equalsign()
then throw SyntaxError // because there are no viable alternatives from here
if not Parse_RHS()
then throw SyntaxError
if not Parse_semicolon()
the throw SyntaxError
return true
}
boolean parse_LHS()
{ if parse_IDENTIFIER()
then return true
else return false
}
boolean parse_RHS()
{ if parse_IDENTIFIER()
then return true
if parse_NUMBER()
then return true
else return false
}
boolean parse_equalsign()
{ if TestInputAndAdvance("=") // this can check for token instead
then return true
else return false
}
boolean parse_semicolon()
{ if TestInputAndAdvance(";")
then return true
else return false
}
boolean parse_IDENTIFIER()
{ if TestInputForIdentifier()
then return true
else return false
}
boolean parse_NUMBER()
{ if TestInputForNumber()
then return true
else return false
}
现在,让我们修改它构建一个抽象语法树:
AST* parse_Goal() // note: we choose to return a null pointer for "false"
{ node = parse_Assignment()
if node != NULL
then return node
else return NULL
}
AST* parse_Assignment()
{ LHSnode = Parse_LHS()
if LHSnode == NULL
then return NULL
EqualNode = Parse_equalsign()
if EqualNode == NULL
then throw SyntaxError // because there are no viable alternatives from here
RHSnode = Parse_RHS()
if RHSnode == NULL
then throw SyntaxError
SemicolonNode = Parse_semicolon()
if SemicolonNode == NULL
the throw SyntaxError
return makeASTNode(ASSIGNMENT,LHSNode,RHSNode)
}
AST* parse_LHS()
{ IdentifierNode = parse_IDENTIFIER()
if node != NULL
then return IdentifierNode
else return NULL
}
AST* parse_RHS()
{ RHSnode = parse_IDENTIFIER()
if RHSnode != null
then return RHSnode
RHSnode = parse_NUMBER()
if RHSnode != null
then return RHSnode
else return NULL
}
AST* parse_equalsign()
{ if TestInputAndAdvance("=") // this can check for token instead
then return makeASTNode("=")
else return NULL
}
AST* parse_semicolon()
{ if TestInputAndAdvance(";")
then return makeASTNode(";")
else return NULL
}
AST* parse_IDENTIFIER()
{ text = TestInputForIdentifier()
if text != NULL
then return makeASTNode("IDENTIFIER",text)
else return NULL
}
AST* parse_NUMBER()
{ text = TestInputForNumber()
if text != NULL
then return makeASTNode("NUMBER",text)
else return NULL
}
我显然掩盖了一些细节,但我认为读者填写它们不会有问题。
JavaCC 和 ANTLR 等解析器生成器工具基本上生成递归下降解析器,并且具有构造与此类似的树的工具。
构建自下而上解析器(YACC、Bison、GLR 等)的解析器生成器工具也以相同的方式构建 AST 节点。但是,没有一组递归函数;取而代之的是,这些工具管理着一堆看到并简化为非终结符的令牌。 AST 节点构建在并行堆栈上;当减少发生时,被减少覆盖的堆栈部分上的 AST 节点被组合以产生一个非终端 AST 节点来替换它们。这发生在语法规则的“零大小”堆栈段也为空时,导致 AST 节点(通常用于“空列表”或“缺少选项”)似乎不知从何出现。
使用小语言,编写构建树的递归下降解析器非常实用。
真正的语言(无论是像 COBOL 那样陈旧而陈旧,还是像 Scala 那样炙手可热)的一个问题是语法规则的数量非常多,而且由于语言的复杂性以及对任何语言委员会负责的坚持而变得复杂它不断添加其他语言提供的新好东西(“语言嫉妒”,参见 Java、C# 和 C++ 之间的进化竞赛)。现在编写递归下降解析器已经失控,人们倾向于使用解析器生成器。但即使使用解析器生成器,编写所有自定义代码来构建 AST 节点也是一场大战(我们还没有讨论过如何设计一个好的“抽象”语法与首先想到的东西)。随着规模的扩大和不断的发展,维护语法规则和构建 AST 变得越来越困难。 (如果你的语言是成功的,一年之内你会想要改变它)。因此,即使编写 AST 构建规则也会变得很尴尬。
理想情况下,人们只想写一个语法,并获得一个解析器和树。你can do this with some recent parser generators: Our DMS Software Reengineering Toolkit accepts full context free grammars, and automatically constructs an AST,语法工程师没有工作;自 1995 年以来一直在这样做。ANTLR 家伙终于在 2014 年发现了这一点,而 ANTLR4 现在提供了这样的选项。
最后一点:拥有解析器(即使使用 AST)几乎不能解决您打算解决的实际问题,无论它是什么。它只是一个基础部分,令大多数解析器新手感到震惊,它是操作代码的工具的最小部分。谷歌我关于解析后的生活的文章(或查看我的简历)以获取更多详细信息。