解析,哪种方法选择?

时间:2013-04-17 17:32:49

标签: parsing compiler-construction grammar top-down

我正在编写一个编译器(接近C的语言),我将在C中实现它。我的主要问题是如何选择正确的解析方法,以便在编译编译器时有效。

这是我目前的语法: http://img11.hostingpics.net/pics/273965Capturedcran20130417192526.png

我正在考虑制作一个自上而下的解析器LL(1),如下所述:http://dragonbook.stanford.edu/lecture-notes/Stanford-CS143/07-Top-Down-Parsing.pdf

考虑到这个语法,它是否是一个有效的选择,因为我知道我首先必须删除左递归规则。你还有其他建议吗?

谢谢你, Mentinet

4 个答案:

答案 0 :(得分:2)

构建解析器的最有效方法是使用一个特定的工具,其存在的目的是构建解析器。它们曾经被称为编译器编译器,但现在焦点已经转移(扩展)到语言工作台,这为您提供了更多帮助来构建您自己的语言。例如,几乎任何语言工作台都可以通过查看语法为您的语言提供IDE支持和语法高亮显示。他们也非常有助于调试你的语法和语言(你没想到左递归是你问题中最大的问题,是吗?)。

在当前支持和开发最好的语言工作台中,可以命名为:

如果您真的如此倾向,或者您考虑自己编写解析器只是为了娱乐和体验,那么最好的现代算法是SGLRGLLPackrat。其中每一项都是持续了半个世纪的算法研究的精髓,所以不要期望在瞬间完全理解它们,并且不要指望任何好处从最初的几个“修复”出来你会出现用。但是,如果您确实想出了一个很好的改进,请不要犹豫与作者分享您的发现或以其他方式发布它!

答案 1 :(得分:2)

这里有很多答案,但是让事情变得困惑。是的,有LL和LR解析器,但这不是你真正的选择。

你有语法。有些工具会根据语法自动为您创建解析器。尊敬的YaccBison就是这么做的。他们创建了一个LR解析器(实际上是LALR)。还有一些工具可以为您创建LL解析器,例如ANTLR。像这样的工具的缺点是它们不灵活。他们自动生成的语法错误消息很糟糕,错误恢复很难,而旧版本鼓励您以一种方式对代码进行分解 - 这恰好是错误的方式。正确的方法是让解析器吐出抽象语法树,然后让编译器从中生成代码。这些工具希望您混合使用解析器和编译器代码。

当您使用这样的自动化工具时,LL,LR和LALR之间的功率差异确实很重要。你不能“欺骗”以扩大他们的权力。 (在这种情况下,功能意味着能够为有效的上下文无关语法生成解析器。有效的上下文无关语法是为每个输入生成唯一,正确的解析树,或正确地说它与语法不匹配的语法。)我们目前没有可以为每个有效语法创建解析器的解析器生成器。但LR可以处理比任何其他类型更多的语法。无法处理语法并不是一场灾难,因为您可以以解析器生成器可以接受的形式重新编写语法。然而,应该如何完成这一点并不总是显而易见的,更糟糕​​的是它会影响生成的抽象语法树,这意味着解析器中的弱点会影响到代码的其余部分 - 比如编译器。

LL,LALR和LR解析器的原因很久以前,生成LR解析器的工作在时间和内存方面对现代计算机都很重要。 (注意这是生成解析器所需要的,只有在你编写它时才会发生。生成的解析器运行得非常快。)但这是一段时间以前。对于中等复杂的语言,生成LR(1)解析器所需的RAM远小于1GB,而在现代计算机上则需要不到一秒钟。因此,使用LR自动解析器生成器(例如Hyacc)会好得多。

另一个选择是编写自己的解析器。在这种情况下,只有一个选择:LL解析器。当这里的人说写LR很难,他们低估了这个案子。人类几乎不可能手动创建LR解析器。您可能认为这意味着如果您编写自己的解析器,则限制使用LL(1)语法。但事实并非如此。既然你正在编写代码,你可以作弊。你可以预见任意数量的符号,并且因为你没有输出任何东西,直到你很好并准备好抽象语法树不必匹配你正在使用的语法。这种作弊能力弥补了LL和LR(1)之间的所有失去的力量,并且通常是一些。

手写解析器当然有其缺点。无法保证您的解析器实际上与您的语法匹配,或者就此而言,不检查您的语法是否有效(即识别您认为它的语言)。它们更长,并且在鼓励您将解析代码与编译代码混合时更糟糕。它们显然也只用一种语言实现,而解析器生成器通常用几种不同的语言吐出它们的结果。即使它们不这样做,LR解析表也可以在仅包含常量的数据结构中表示(例如在JSON中),并且实际的解析器只有100行代码。但手写解析器也有好处。因为您编写了代码,所以您知道发生了什么,因此更容易进行错误恢复并生成合理的错误消息。

最后,权衡通常是这样的:

  • 对于一次性工作,使用LR(1)解析器生成器要好得多。生成器将检查你的语法,保存你的工作,现代的生成器直接拆分抽象语法树,这正是你想要的。
  • 对于高度抛光的工具,如mcc或gcc,请使用手写的LL解析器。无论如何,您将编写大量的单元测试以保护您的背部,错误恢复和错误消息更容易正确,并且他们可以识别更大类的语言。

我唯一的另一个问题是:为什么是C?编译器通常不是时间关键代码。有非常好的解析包可以让你在代码的1/2中完成工作,如果你愿意让你的编译器运行得慢一点 - 例如我自己的Lrparsing。请记住,“慢一点”这意味着“人类几乎不会注意到”。我想答案是“我正在进行的任务指定C”。为了给你一个想法,这就是当你放松要求时,从语法到解析树变得多么简单。这个计划:

#!/usr/bin/python

from lrparsing import *

class G(Grammar):
  Exp = Ref("Exp")
  int = Token(re='[0-9]+')
  id = Token(re='[a-zA-Z][a-zA-Z0-9_]*')
  ActArgs = List(Exp, ',', 1)
  FunCall = id + '(' + Opt(ActArgs) + ')'
  Exp = Prio(
      id | int | Tokens("[]", "False True") | Token('(') + List(THIS, ',', 1, 2) + ')' |
      Token("! -") + THIS,
      THIS << Tokens("* / %") << THIS,
      THIS << Tokens("+ -") << THIS,
      THIS << Tokens("== < > <= >= !=") << THIS,
      THIS << Tokens("&&") << THIS,
      THIS << Tokens("||") << THIS,
      THIS << Tokens(":") << THIS)
  Type = (
      Tokens("", "Int Bool") |
      Token('(') + THIS + ',' + THIS + ')' |
      Token('[') + THIS + ']')
  Stmt = (
      Token('{') + THIS * Many + '}' |
      Keyword("if") + '(' + Exp + ')' << THIS + Opt(Keyword('else') + THIS) |
      Keyword("while") + '(' + Exp + ')' + THIS |
      id + '=' + Exp + ';' |
      FunCall + ';' |
      Keyword('return') + Opt(Exp) + ';')
  FArgs = List(Type + id, ',', 1)
  RetType = Type | Keyword('void')
  VarDecl = Type + id + '=' + Exp + ';'
  FunDecl = (
      RetType + id + '(' + Opt(FArgs) + ')' +
      '{' + VarDecl * Many + Stmt * Some + '}')
  Decl = VarDecl | FunDecl
  Prog = Decl * Some
  COMMENTS = Token(re="/[*](?:[^*]|[*][^/])*[*]/") | Token(re="//[^\n\r]*")
  START = Prog

EXAMPLE = """\
Int factorial(Int n) {
  Int result = 1;
  while (n > 1) {
    result = result * n;
    n = n - 1;
  }
  return result;
}
"""

parse_tree = G.parse(EXAMPLE)
print G.repr_parse_tree(parse_tree)

生成此输出:

(START (Prog (Decl (FunDecl
  (RetType (Type 'Int'))
  (id 'factorial') '('
  (FArgs
    (Type 'Int')
    (id 'n')) ')' '{'
  (VarDecl
    (Type 'Int')
    (id 'result') '='
    (Exp (int '1')) ';')
  (Stmt 'while' '('
    (Exp
      (Exp (id 'n')) '>'
      (Exp (int '1'))) ')'
    (Stmt '{'
      (Stmt
        (id 'result') '='
        (Exp
          (Exp (id 'result')) '*'
          (Exp (id 'n'))) ';')
      (Stmt
        (id 'n') '='
        (Exp
          (Exp (id 'n')) '-'
          (Exp (int '1'))) ';') '}'))
  (Stmt 'return'
    (Exp (id 'result')) ';') '}'))))

答案 2 :(得分:1)

感谢所有这些建议,但我们最终决定使用与此处完全相同的方法构建我们自己的递归下降解析器:http://www.cs.binghamton.edu/~zdu/parsdemo/recintro.html

实际上,我们更改了语法以删除左递归规则,因为我在第一条消息中显示的语法不是LL(1),我们使用我们的令牌列表(由我们的扫描仪制作)来继续前瞻更进一步。它看起来效果很好。

现在我们在这些递归函数中构建了一个AST。你有什么建议吗?提示?非常感谢你。

答案 3 :(得分:0)

最有效的解析器是LR-Parsers和LR解析器有点难以实现。您可以使用递归下降解析技术,因为它更容易在C中实现。