it-swarm.cn

我应该使用解析器生成器还是应该滚动自己的自定义词法分析器和解析器代码?

编程语言语法的每种方法有哪些specific优点和缺点?

为什么/何时应该自己滚?为什么/何时应该使用发电机?

83
Maniero

确实有三个选择,这三个选择在不同情况下更可取。

选项1:解析器生成器,或者“您需要解析某种语言,而只是想让它正常工作,该死”

假设您现在被要求为某些古老的数据格式构建解析器。或者您需要解析器要快。或者,您需要解析器易于维护。

在这些情况下,最好使用解析器生成器。您不必费心处理所有细节,也不必获取大量复杂的代码即可正常工作,只需编写输入将遵循的语法,编写一些处理代码并保存:即时解析器。

优点很明显:

  • (通常)编写规范非常容易,尤其是在输入格式不太奇怪的情况下(如果选择2,则更好)。
  • 您最终得到了一个非常容易维护的工作,并且易于理解:语法定义通常比代码自然得多。
  • 好的解析器生成器生成的解析器通常比手写代码快很多。手写代码可以更快,但是只有在您了解自己的知识的情况下-这就是为什么使用最广泛的编译器使用手写递归下降解析器的原因。

使用解析器生成器时,需要注意一件事:有时可能会拒绝您的语法。有关语法分析器的不同类型以及它们如何咬您的概述,您可能需要启动 此处这里 您可以找到许多实现的概述以及它们接受的语法类型。

选项2:手写解析器,或者“您想构建自己的解析器,并且关心用户友好”

解析器生成器很好,但是它们对用户(最终用户而不是您)不是很友好。通常,您不能提供良好的错误消息,也不能提供错误恢复功能。也许您的语言很奇怪,并且解析器拒绝了您的语法,或者您需要的控制权超出了生成器所能提供的范围。

在这些情况下,最好使用手写递归下降解析器。尽管正确处理可能很复杂,但是您可以完全控制解析器,因此可以执行解析器生成器无法完成的各种不错的工作,例如错误消息甚至错误恢复(尝试从C#文件中删除所有分号) :C#编译器会抱怨,但是无论分号是否存在,无论如何都会检测到其他大多数错误)。

假设解析器的质量足够高,手写解析器通常也比生成的解析器性能更好。另一方面,如果您通常由于缺乏经验,知识或设计而导致编写失败的解析器(通常是由于这些的结合),则性能通常会变慢。对于词法分析器而言,情况恰恰相反:通常生成的词法分析器使用表查找,从而使它们比(大多数)手写查询更快。

在教育方面,编写自己的解析器将比使用生成器教给您更多的知识。毕竟,您必须编写越来越复杂的代码,另外,您还必须确切地了解如何解析语言。另一方面,如果您想学习如何创建自己的语言(因此,要获得语言设计经验),则最好选择选项1或选项3:如果要开发一种语言,它可能会发生很大变化,而选项1和3则使您的工作更轻松。

选项3:手写的解析器生成器,或者“您正在尝试从该项目中学到很多东西,并且您不介意以大量可以重复使用的漂亮代码结尾”

这是我当前要走的路:您编写您自己的解析器生成器。虽然非常简单,但是这样做可能会教给您最多的知识。

为了让您了解进行此类项目所涉及的内容,我将向您介绍自己的进度。

词法生成器

我首先创建了自己的词法生成器。我通常从开始使用代码开始设计软件,所以我考虑了如何使用我的代码并编写了这段代码(在C#中):

Lexer<CalculatorToken> calculatorLexer = new Lexer<CalculatorToken>(
    new List<StringTokenPair>()
    { // This is just like a Lex specification:
      //                    regex   token
        new StringTokenPair("\\+",  CalculatorToken.Plus),
        new StringTokenPair("\\*",  CalculatorToken.Times),
        new StringTokenPair("(",    CalculatorToken.LeftParenthesis),
        new StringTokenPair(")",    CalculatorToken.RightParenthesis),
        new StringTokenPair("\\d+", CalculatorToken.Number),
    });

foreach (CalculatorToken token in
             calculatorLexer.GetLexer(new StringReader("15+4*10")))
{ // This will iterate over all tokens in the string.
    Console.WriteLine(token.Value);
}

// Prints:
// 15
// +
// 4
// *
// 10

使用算术堆栈的思想,将输入的字符串-令牌对转换为相应的递归结构,以描述它们表示的正则表达式。然后将其转换为NFA(非确定性有限自动机),然后将其转换为DFA(确定性有限自动机)。然后,您可以将字符串与DFA进行匹配。

这样,您就可以很好地了解词法分析器的工作原理。此外,如果以正确的方式进行操作,则词法分析器生成器的结果可以与专业实现大致一样快。与选项2相比,您也不会失去任何表现力,与选项1相比,您不会失去太多表现力。

我用1600多行代码实现了lexer生成器。这段代码可以完成上述工作,但是每次启动程序时它仍会即时生成词法分析器:在某些时候,我将添加代码以将其写入磁盘。

如果您想知道如何编写自己的词法分析器,那么 this 是一个不错的起点。

解析器生成器

然后,您编写解析器生成器。我再次参考 这里 来获得有关各种解析器的概述-根据经验,它们可以解析的越多,速度就越慢。

速度对我来说不是问题,我选择实现Earley解析器。 Earley解析器的高级实现 已显示 大约是其他解析器类型的两倍。

作为这种快速打击的回报,您可以解析any一种语法,甚至是模棱两可的语法。这意味着您无需担心解析器中是否存在任何左递归,或者移位-减少冲突是什么。如果结果的语法树无关紧要,也可以使用歧义语法更轻松地定义语法,例如,将1 + 2 + 3解析为(1 + 2)+3还是1都无关紧要+(2 + 3)。

这是使用我的解析器生成器的一段代码如下所示:

Lexer<CalculatorToken> calculatorLexer = new Lexer<CalculatorToken>(
    new List<StringTokenPair>()
    {
        new StringTokenPair("\\+",  CalculatorToken.Plus),
        new StringTokenPair("\\*",  CalculatorToken.Times),
        new StringTokenPair("(",    CalculatorToken.LeftParenthesis),
        new StringTokenPair(")",    CalculatorToken.RightParenthesis),
        new StringTokenPair("\\d+", CalculatorToken.Number),
    });

Grammar<IntWrapper, CalculatorToken> calculator
    = new Grammar<IntWrapper, CalculatorToken>(calculatorLexer);

// Declaring the nonterminals.
INonTerminal<IntWrapper> expr = calculator.AddNonTerminal<IntWrapper>();
INonTerminal<IntWrapper> term = calculator.AddNonTerminal<IntWrapper>();
INonTerminal<IntWrapper> factor = calculator.AddNonTerminal<IntWrapper>();

// expr will be our head nonterminal.
calculator.SetAsMainNonTerminal(expr);

// expr: term | expr Plus term;
calculator.AddProduction(expr, term.GetDefault());
calculator.AddProduction(expr,
                         expr.GetDefault(),
                         CalculatorToken.Plus.GetDefault(),
                         term.AddCode(
                         (x, r) => { x.Result.Value += r.Value; return x; }
                         ));

// term: factor | term Times factor;
calculator.AddProduction(term, factor.GetDefault());
calculator.AddProduction(term,
                         term.GetDefault(),
                         CalculatorToken.Times.GetDefault(),
                         factor.AddCode
                         (
                         (x, r) => { x.Result.Value *= r.Value; return x; }
                         ));

// factor: LeftParenthesis expr RightParenthesis
//         | Number;
calculator.AddProduction(factor,
                         CalculatorToken.LeftParenthesis.GetDefault(),
                         expr.GetDefault(),
                         CalculatorToken.RightParenthesis.GetDefault());
calculator.AddProduction(factor,
                         CalculatorToken.Number.AddCode
                         (
                         (x, s) => { x.Result = new IntWrapper(int.Parse(s));
                                     return x; }
                         ));

IntWrapper result = calculator.Parse("15+4*10");
// result == 55

(请注意,IntWrapper只是一个Int32,除了C#要求它是一个类,因此我不得不引入一个包装器类)

我希望您看到上面的代码非常强大:可以解析出您能想到的任何语法。您可以在语法中添加任意代码位以执行许多任务。如果您设法使所有这些工作正常进行,则可以非常轻松地重用生成的代码来完成许多任务:想象一下使用此代码构建命令行解释器。

78
Alex ten Brink

如果您从未写过解析器,我建议您这样做。这很有趣,并且您学习了事物的工作方式,并且了解了解析器和词法分析器生成器为您节省了所需的next时间的工作量解析器。

我还建议您尝试阅读 http://compilers.iecc.com/crenshaw/ ,因为它对如何执行具有非常扎实的态度。

22
user1249

编写自己的递归下降解析器的优点是,您可以在语法错误上生成高质量错误消息。使用解析器生成器,可以产生错误并在某些点添加自定义错误消息,但是解析器生成器不具备完全控制解析的能力。

编写自己的代码的另一个好处是,它更容易解析为一个与语法没有一对一对应关系的简单表示形式。

如果语法是固定的,并且错误消息很重要,请考虑自己滚动,或者至少使用可以为您提供所需错误消息的解析器生成器。如果语法在不断变化,则应考虑使用解析器生成器。

Bjarne Stroustrup谈论了他是如何使用YACC进行C++的第一个实现的(请参阅C++的设计和演进)。在第一种情况下,他希望自己编写自己的递归下降解析器!

14
Macneil

选项3:都不(滚动您自己的解析器生成器)

只是因为有理由不使用 [〜#〜] antlr [〜#〜]野牛Coco/RGrammaticaJavaCC柠檬已煮熟SableCCQuexetc-这并不意味着您应该立即滚动自己的解析器+词法分析器。

确定为什么所有这些工具还不够好-为什么它们不能让您实现目标?

除非您确定所处理的语法中的奇数是唯一的,否则您不应该只为此创建一个自定义解析器+语法分析器。相反,创建一个可以创建所需内容的工具,但该工具也可以用于满足将来的需求,然后将其作为免费软件发布,以防止其他人遇到与您相同的问题。

10
Peter Boughton

滚动自己的解析器会迫使您直接考虑语言的复杂性。如果该语言难以解析,则可能很难理解。

早期,由于高度复杂(有些人会说“折磨”)的语言语法,解析器生成器引起了人们的极大兴趣。 JOVIAL是一个特别糟糕的示例:它需要提前两个符号,而其他所有内容最多都需要一个符号。这使得为​​JOVIAL编译器生成解析器比预期的要困难得多(因为General Dynamics/Fort Worth Division在为F-16程序购买JOVIAL编译器时学到了很难的方法)。

如今,递归下降是普遍首选的方法,因为它对编译器编写者来说更容易。递归下降编译器极大地奖励了简单,简洁的语言设计,因为为一种简单,干净的语言编写一个递归下降解析器要比为那些复杂的,混乱的语言编写代码容易得多。

最后:您是否考虑过将语言嵌入LISP中,并让LISP解释器为您完成繁重的工作? AutoCAD做到了这一点,发现这使他们的生活变得更加轻松。有很多轻量级的LISP解释器,有些是可嵌入的。

8
John R. Strohm

我曾经为商业应用编写过一个解析器,然后使用yacc。有一个竞争的原型,其中开发人员使用C++手工编写了整个程序,并且工作速度慢了大约五倍。

至于该解析器的词法分析器,我完全是手工编写的。抱歉-差不多是十年前了,所以我记不清了-[〜#〜] c [〜#〜]中有大约1000行。

我之所以手动编写词法分析器的原因是解析器的输入语法。这是我的解析器实现必须遵守的要求,而不是我设计的要求。 (当然,我会以不同的方式设计它。而且更好!)语法在某些地方严格依赖于上下文,甚至词法依赖于语义。例如,分号可以在一处成为令牌的一部分,而在另一处成为分隔符-基于对以前解析出的某个元素的语义解释。因此,我在手写词法分析器中“掩埋”了这种语义依赖性,这给我留下了相当直接的[〜#〜] bnf [〜#〜]在yacc中易于实现。

[〜#〜]添加[〜#〜]以响应Macneil:yacc提供了一种非常强大的抽象,可让程序员从终端,非终端,作品之类的东西。另外,在实现yylex()函数时,它帮助我专注于返回当前令牌,而不必担心它之前或之后的内容。 C++程序员在字符级别上工作,而没有这种抽象的好处,最终创建了一个更复杂,效率更低的算法。我们得出的结论是,较慢的速度与C++本身或任何库无关。我们使用加载到内存中的文件测量了纯解析速度;如果我们遇到文件缓冲问题,则yacc并不是解决问题的首选工具。

也想添加:一般而言,这并不是编写解析器的诀窍,只是它在特定情况下如何工作的一个示例。

6
azheglov

这取决于您的目标是什么。

您是否要学习解析器/编译器的工作方式?然后从头开始编写自己的内容。那是您真正学会欣赏他们正在做的所有事情的唯一方法。在过去的几个月中,我一直在写作,这是一次有趣而宝贵的经历,尤其是“啊,所以这就是为什么X语言做到这一点……”的时刻。

您是否需要在截止日期之前快速将某些东西放在一起以进行应用?然后也许使用解析器工具。

您是否需要在未来10年,20年甚至30年中扩展的东西?自己写,慢慢来。这将是非常值得的。

3
GrandmasterB

这完全取决于您需要解析的内容。您能否以自己的速度超过词法学习器的学习曲线?被解析的内容是否足够静态,以至于您以后不会后悔该决定?您是否发现现有的实施方案过于复杂?如果是这样,那就滚滚自己的乐子吧,但前提是您不要回避学习曲线。

最近,我变得非常喜欢 lemon parser ,可以说这是我用过的最简单,最简单的方法。为了使事情易于维护,我只将其用于大多数需求。 SQLite使用它以及其他一些著名的项目。

但是,我对词法分析器一点都不感兴趣,除此之外,当我需要使用一个词法分析器(因此,柠檬)时,它们不会妨碍我。你可能是,如果是这样,为什么不做一个?我有一种感觉,您会回到使用现有的那种方法,但是如果必须的话,请挠痒痒:)

3
Tim Post

您是否考虑过 马丁·福勒斯语言工作台方法 ?引用本文

语言工作台对方程式所做的最明显改变是创建外部DSL的简便性。您不再需要编写解析器。您必须定义抽象语法-但这实际上是一个非常简单的数据建模步骤。另外,您的DSL获得了强大的IDE-尽管您确实需要花费一些时间来定义该编辑器。生成器仍然是您必须要做的事情,而我的感觉是它并不多比以往任何时候都更容易,但是为良好而简单的DSL构建生成器是该练习最容易的部分之一。

读完这些,我会说编写自己的解析器的日子已经过去了,最好使用可用的一种库。掌握了库之后,您将来创建的所有DSL都将从该知识中受益。另外,其他人也不必学习您的解析方法。

编辑以覆盖评论(和修订的问题)

自己滚动的优点

  1. 您将拥有解析器,并通过一系列复杂的问题获得所有可爱的思考经验
  2. 您可能会想到其他人没有想到的特殊内容(不太可能,但您看起来像是聪明的家伙)
  3. 它会让您忙于一个有趣的问题

简而言之,当您想深入探究一个强烈的动机要解决的严重难题时,应该自己动手做。

使用别人的图书馆的好处

  1. 您将避免重新发明轮子(您会同意的编程中的常见问题)
  2. 您可以专注于最终结果(您使用的是崭新的语言),而不必担心它的解析方式等
  3. 您会更快地看到自己的语言(但您获得的回报会更少,因为这不是全部)

因此,如果您想获得快速的最终结果,请使用其他人的资料库。

总体而言,这取决于您要解决问题的数量以及解决方案的选择。如果您想要全部,请自己动手。

3
Gary Rowe

编写自己的书的最大好处是,您将知道如何编写自己的书。使用yacc之类的工具的最大好处是,您将知道如何使用该工具。我是 treetop 的爱好者,可以进行初步探索。

2
philosodad

为什么不派生一个开放源代码的解析器生成器并使其成为自己的呢?如果不使用解析器生成器,则如果对语言的语法进行了较大的更改,则代码将很难维护。

在解析器中,我使用正则表达式(我的意思是Perl风格)来标记化,并使用一些便利函数来提高代码的可读性。但是,解析器生成的代码可以通过制作状态表和较长的switch-_cases来加快速度,这可能会增加源代码的大小,除非您使用.gitignore他们。

这是我的自定义解析器的两个示例:

https://github.com/SHiNKiROU/DesignScript -一种BASIC方言,因为我太懒了,无法以数组符号编写先行方式,所以我牺牲了错误消息的质量 https:// github。 com/SHiNKiROU/ExprParser -一个公式计算器。注意奇怪的元编程技巧

1
Ming-Tang

“我应该使用这个久经考验的'轮子'还是重新发明它?”

0
JBRWilkinson