手动编写解析器的最佳方法是什么?

时间:2009-02-10 09:49:20

标签: c# .net parsing f#

我们使用ANTLR为类似SQL的语法创建解析器,虽然在大多数情况下结果令人满意,但我们需要修复一些边缘情况。由于我们自己没有编写解析器,因此我们并不能很好地理解它,以便能够做出明智的改变。

所以,我们想编写自己的解析器。手动编写解析器的最佳方法是什么?我们应该使用什么样的解析器 - 建议使用递归下降;是对的吗?我们将用C#编写它,所以任何用这种语言编写解析器的教程都会感激不尽。

更新:我也对涉及F#的答案感兴趣 - 我一直在寻找在项目中使用它的理由。

16 个答案:

答案 0 :(得分:17)

我强烈建议使用F#语言作为在.NET平台上解析的首选语言。它是ML系列语言的根源,意味着它对面向语言的编程提供了极好的支持。

歧视的联合和模式匹配允许您的AST非常简洁和强大的规范。高阶函数允许定义解析操作及其组成。对monadic类型的一流支持允许隐式处理状态管理,大大简化了解析器的组成。强大的类型推断极大地帮助了这些(复杂)类型的定义。所有这一切都可以通过交互方式指定和执行,让您快速制作原型。

Stephan Tolksdorf通过他的解析器组合库FParsec

将其付诸实践

从他的例子中我们可以看出AST的指定是多么自然:

type expr =
    | Val of string
    | Int of int
    | Float of float
    | Decr of expr

type stmt =
    | Assign of string * expr
    | While of expr * stmt
    | Seq of stmt list
    | IfThen of expr * stmt
    | IfThenElse of expr * stmt * stmt
    | Print of expr

type prog = Prog of stmt list

解析器的实现(部分省略)就像简洁:

let stmt, stmtRef = createParserForwardedToRef()

let stmtList = sepBy1 stmt (ch ';')

let assign =
    pipe2 id (str ":=" >>. expr) (fun id e -> Assign(id, e))

let print = str "print" >>. expr |>> Print

let pwhile =
    pipe2 (str "while" >>. expr) (str "do" >>. stmt) (fun e s -> While(e, s))

let seq =
    str "begin" >>. stmtList .>> str "end" |>> Seq

let ifthen =
    pipe3 (str "if" >>. expr) (str "then" >>. stmt) (opt (str "else" >>. stmt))
          (fun e s1 optS2 ->
               match optS2 with
               | None    -> IfThen(e, s1)
               | Some s2 -> IfThenElse(e, s1, s2))

do stmtRef:= choice [ifthen; pwhile; seq; print; assign]


let prog =
    ws >>. stmtList .>> eof |>> Prog

在第二行,作为一个例子,stmtch是解析器,sepBy1是一个monadic解析器组合器,它接受两个简单的解析器并返回一个组合解析器。在这种情况下,sepBy1 p sep返回一个解析器,该解析器解析由p分隔的sep个出现的一个或多个事件。因此,您可以看到从简单的解析器中组合强大的解析器的速度有多快。 F#对重写的运算符的支持也允许简洁的中缀表示法,例如:序列组合子和选择组合子可以指定为>>.<|>

祝你好运,

丹尼

答案 1 :(得分:10)

唯一可以由理智的人手写的解析器是递归下降。也就是说,手写自下而上的解析器仍然是可能的,但这是非常不可取的。

如果您正在使用RD解析器,则必须验证SQL语法不是左递归的(并在必要时消除递归),然后基本上为每个语法规则编写一个函数。有关详细信息,请参阅this

答案 2 :(得分:7)

递归下降会给你最简单的方法,但我不得不同意mouviciel那种flex和bison并且绝对值得学习。当你发现你的语法有错误时,在flex / bison中修复语言的定义将会比重写你的递归下降代码容易得多。

仅供参考,C#解析器是递归下降的,它往往非常健壮。

答案 3 :(得分:7)

将我的声音添加到合唱中,支持递归下降(LL1)。它们简单,快速,而且IMO,完全不难维护。

但是,请仔细查看您的语言,确保它是LL1。如果您有像C这样的语法,例如((((type))foo)[]),您可能需要先删除多层括号,然后才能发现是否正在查看类型,变量或表达式,那么LL1将非常困难,而自下而上的胜利。

答案 4 :(得分:6)

递归下降解析器确实是最好的,也许只有解析器,可以手工构建。您仍然需要了解正式的,无上下文的语言,并将您的语言置于正常形式。我个人建议您删除左递归并将您的语言放在Greibach Normal Form中。当你这样做时,解析器只是写自己。

例如,这个制作:

A => aC 
A => bD
A => eF

变得简单:

int A() {
   chr = read();
   switch char
     case 'a': C();
     case 'b': D();
     case 'e': F();
     default: throw_syntax_error('A', chr);
}

这里没有更难的案例(更难的是确保你的语法形式正确,但这可以让你控制你所提到的。)

Anton's Link似乎也很出色。

答案 5 :(得分:3)

如果你想手工编写,递归正确是最明智的选择。

您可以使用表格解析器,但这将非常难以维护。

示例:

Data = Object | Value;
Value = Ident, '=', Literal;
Object = '{', DataList, '}';
DataList = Data | DataList, Data;


ParseData {
  if PeekToken = '{' then 
    return ParseObject;
  if PeekToken = Ident then
    return ParseValue;
  return Error;
}

ParseValue {
  ident = TokenValue;
  if NextToken <> '=' then 
    return Error;
  if NextToken <> Literal then
    return Error;
  return(ident, TokenValue);
 }

ParseObject {
  AssertToken('{');
  temp = ParseDataList;
  AssertToken('}');
  return temp;
}

ParseDataList {
  data = ParseData;
  temp = []
  while Ok(data) {
    temp = temp + data;
    data = ParseData;
  }
}

答案 6 :(得分:2)

我建议你不要手写lexer - 使用flex或类似的。识别令牌的任务并不难以手工完成,但我认为你不会获得太多收益。

正如其他人所说,递归下降解析器最容易手写。否则,您必须维护每个令牌的状态转换表,这不是人类可读的。

我非常确定ANTLR实现了一个递归下降解析器:提到它in an interview about ANTLR 3.0

我还发现了一系列关于writing a parser in C#的博客文章。看起来很温柔。

答案 7 :(得分:2)

冒着冒犯OP的风险,当有好的解析器生成器工具(例如ANTLR)可用时,手动为一些特定供应商的SQL编写一个大型语言解析器,这简直太疯狂了。与使用解析器生成器修复“边缘情况”相比,您将花费更多时间手动重写解析器,并且随着SQL标准的移动或者您发现您被误解,您将不得不返回并修改解析器别的。如果您不能很好地理解您的解析技术,请花时间去理解它。使用解析器生成器来计算如何处理边缘情况并不需要花费数月时间,并且您已经承认它愿意花费数月时间手动执行它。

话虽如此,如果你一心想要手动重做它,使用递归下降解析器是手动完成它的最好方法。

(注意:OP在下面澄清了他想要一个参考实现。恕我直言,你不应该在程序上编写语法的参考实现,所以递归下降了。)

答案 8 :(得分:2)

没有“最好的”方式。根据您的需要,您可能需要自下而上(LALR1)或递归下降(LLk)。文章such as this one给出了个人原因,希望将LALR(1)(自下而上)改为LL(k)。但是,每种类型的解析器都有其优点和缺点。通常,LALR会更快,因为生成finite-state_machine作为查找表。

选择适合你的方法来检查你的情况;熟悉工具和技术。从一些LALRLL维基百科文章开始并不是一个糟糕的选择。在这两种情况下,您都应该始终在BNFEBNF中指定语法。我更喜欢EBNF的简洁性。

一旦你完成了你想要做的事情,以及如何将其表示为语法,(BNF或EBNF)尝试使用几种不同的工具,并在要解析的代表性文本样本中运行它们。

<强>据传:

但是,我听说LL(k)更灵活。我从来没有费心去找自己。从我的几个解析器构建体验中我注意到,如果它是LALR或LL(k),那么选择最适合您需求的最佳方式就是从编写语法开始。我编写了自己的C ++ EBNF RD解析器构建器模板库,使用了Lex / YACC并编写了一个小型R-D解析器。这是在15年的大部分时间里传播的,我在这三个项目中花费的时间不超过2个月。

答案 9 :(得分:1)

在C / Unix中,传统的方法是使用lex和yacc。使用GNU,相同的工具是flex和bison。我不知道Windows / C#。

答案 10 :(得分:1)

如果我是你,我会在ANTLRv3上再使用GUI ANTLRWorks,它为您提供了一种非常方便的语法测试方法。我们在项目中使用ANTLR,虽然学习曲线在开始时可能会有点陡峭但是很方便。另外,在他们的电子邮件通讯中,有很多人非常乐于助人。

PS。 IIRC他们也有一个你可以看一下的SQL语法。

HTH

答案 11 :(得分:0)

如果您不介意使用其他编译器编译器工具,例如 ANTLR ,我建议您查看Coco/R

我过去曾经使用它,它非常好......

答案 12 :(得分:0)

您不希望使用表驱动的解析器的原因是您将无法创建合理的错误消息。这对于生成的语言是可以的,但不适用于涉及人类的语言。由类似c语言的编译器生成的错误消息提供了充分的证据,表明人们可以适应任何事情,无论多么糟糕。

答案 13 :(得分:0)

我也会投票使用现有的解析器+词法分析器。

我亲眼看到的唯一原因是:

  • 如果你想要一些相对简单的东西(比如验证/解析一些输入)
  • 学习/理解原则。

答案 14 :(得分:0)

JFlex是Java的flex实现,现在有一个该项目的{C#端口http://sourceforge.net/projects/csflex/。似乎还有一个正在进行CUP的C#端口,可在此处找到:http://sourceforge.net/projects/datagraph/

我也建议避免手工制作自己的解决方案。我曾经尝试过一次非常简单的语言(大学项目的一部分),这非常耗时且困难。一旦写完,维护和改变也非常困难。

使用现有的解析器生成器是可行的方法,因为大量的艰苦工作已经完成并且多年来经过了充分的测试。

答案 15 :(得分:0)

查看.NET的gplex和gppg,lexer和parser生成器。它们运行良好,并且基于与lex和yacc相同(并且几乎兼容)的输入,这相对容易使用。