パーサージェネレーターを使用する必要がありますか、それとも独自のカスタムレクサーとパーサーコードをロールする必要がありますか?


回答:


78

本当に3つのオプションがあり、それらの3つはすべて異なる状況で望ましいものです。

オプション1:パーサージェネレーター

たとえば、古代のデータ形式のパーサーを今すぐ構築するように求められます。または、パーサーを高速にする必要があります。または、パーサーを簡単に保守できる必要があります。

これらの場合、おそらくパーサージェネレーターを使用するのが最善です。詳細をいじる必要はありません。適切に機能するために多くの複雑なコードを取得する必要はありません。入力が順守する文法を書き、処理コードとpresto:インスタントパーサーを書くだけです。

利点は明らかです。

  • (通常)仕様を記述するのは非常に簡単です。特に入力形式があまりにも奇妙でない場合(オプション2がより適切です)。
  • 最終的に、非常に簡単に保守できる作業が理解しやすくなります。通常、文法定義はコードよりもはるかに自然に流れます。
  • 通常、優れたパーサージェネレーターによって生成されるパーサーは、手書きのコードよりもはるかに高速です。手書きコードより高速になりますが、それはあなたが自分のものを知っている場合のみです-これが最も広く使用されているコンパイラが手書きの再帰下降パーサーを使用する理由です。

パーサージェネレーターで注意しなければならないことが1つあります:文法を拒否することがあります。さまざまなタイプのパーサーの概要と、それらがどのように噛み付くかについては、ここから始めてくださいここでは、多くの実装の概要と、それらが受け入れる文法の種類を見つけることができます。

オプション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(非決定性有限オートマトン)に変換され、さらにNFA(決定性有限オートマトン)に変換されます。次に、DFAに対して文字列を照合できます。

これにより、レクサーが正確にどのように機能するかがわかります。さらに、正しい方法で実行すれば、レクサージェネレーターからの結果は、プロフェッショナルな実装とほぼ同じくらい高速になります。また、オプション2と比較して表現力が失われることはなく、オプション1と比較して表現力もあまり失われません。

わずか1600行を超えるコードでレクサージェネレーターを実装しました。このコードは上記の作業を行いますが、プログラムを起動するたびにオンザフライでレクサーを生成します。ある時点でディスクに書き込むコードを追加します。

独自のレクサーの作成方法を知りたい場合は、ここから始めるのがよいでしょう。

パーサージェネレーター

次に、パーサージェネレーターを作成します。さまざまな種類のパーサーの概要については、ここでもう一度参照します。経験則として、解析できるほど速度は遅くなります。

速度は私にとって問題ではないので、Earleyパーサーを実装することにしました。Earleyパーサーの高度な実装は、他のパーサータイプの約2倍遅いことが示さています。

そのスピードヒットの見返りに、どんな種類の文法でも、あいまいなものでも解析できるようになります。これは、パーサーに左再帰があるかどうか、またはシフトとリデュースの競合が何であるかを心配する必要がないことを意味します。また、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#ではクラスである必要があるため、ラッパークラスを導入する必要がありました)

上記のコードが非常に強力であることをご理解いただければ幸いです。思いつく文法はすべて解析できます。多くのタスクを実行できる任意のコードを文法に追加できます。これをすべて機能させることができれば、結果のコードを再利用して非常に簡単に多くのタスクを実行できます。このコードを使用してコマンドラインインタープリターを構築することを想像してください。


3
高性能のパーサーとレクサーを作成するために必要な作業量を過小評価していると思います。

私はすでに独自のレクサージェネレーターの構築を終了しており、代わりに別のアルゴリズムを実装することに決めたとき、独自のパーサージェネレーターの構築にかなりの時間を費やしました。すべてを機能させるのにそれほど時間はかかりませんでしたが、再び「高性能」、「優れた性能」、「優れた漸近性能」を目指していませんでした-Unicodeは優れた実行時間を得るための雌ですまた、C#を使用すると、すでにパフォーマンスのオーバーヘッドが発生します。
アレックス10ブリンク10

とてもいい答えです。私はあなたのオプションNrに同意します。上記のすべての理由により3。しかし、私の場合のように、言語の設計についても非常に真剣に考えている場合は、独自のパーサージェネレーターを作成すると同時にパーサージェネレーターも使用する必要があるかもしれません。だから、あなたは言語の問題で
有利

1
4番目のオプション:パーサーコンビネーターがあります。
ユーリアルバカーキ14

@AlextenBrink偶然githubアカウントを持っていますか?私は本当にそのレクサー/パーサーを手に入れたいです。あなたが作った印象的なもの。
Behrooz

22

パーサーを作成したことがない場合は、これを行うことをお勧めします。それは楽しく、物事がどのように機能するかを学び、パーサーとレクサージェネレーターが次にパーサーを必要とするときの労力を節約できることを学びます。

また、http://compilers.iecc.com/crenshaw/を読むことをお勧めします。http://compilers.iecc.com/crenshaw/は、それを行う方法に対して非常に現実的な態度を持っているからです。


2
良い提案と非常に便利なリンク。
マニエロ

14

独自の再帰降下パーサーを作成する利点は、構文エラーに関する高品質のエラーメッセージを生成できることです。パーサージェネレーターを使用すると、特定の時点でエラーを生成し、カスタムエラーメッセージを追加できますが、パーサージェネレーターは、解析を完全に制御する能力とは一致しません。

独自に記述することのもう1つの利点は、文法と1対1の対応関係を持たない単純な表現に解析しやすいことです。

文法が修正されており、エラーメッセージが重要な場合は、独自のローリングを検討するか、少なくとも必要なエラーメッセージを提供するパーサージェネレーターの使用を検討してください。文法が常に変化している場合は、代わりにパーサージェネレーターの使用を検討する必要があります。

Bjarne Stroustrupは、C ++の最初の実装でYACCをどのように使用したかについて語っています(C ++ の設計と進化を参照)。その最初のケースでは、彼は代わりに彼自身の再帰降下パーサーを書くことを望みました!


最初の実験はパーサージェネレーターを使用する必要があるとはほとんど確信していません。カスタムソリューションに交換することでいくつかの利点がありました。私はまだ何も決定していませんが、私を助けるのに役立つ答えです。
マニエロ

++この答えはまさに私が言うことです。私は数多くの言語を構築してきましたが、ほとんどの場合、再帰下降を使用していました。私は、必要な言語がCまたはC ++(またはLisp)の上にいくつかのマクロを階層化することによって最も簡単に構築されたときがあったことを付け加えます。
マイクダンラベイ

JavaCCには、最高のエラーメッセージがあると主張されています。また、V8およびFirefoxのJavaScriptエラーおよび警告メッセージに注目してください。パーサージェネレーターは使用していません。
明唐

2
@SHiNKiROU:確かに、JavaCCが再帰降下解析を使用することはおそらく偶然ではありません。
マクニール

10

オプション3:どちらでもない(独自のパーサージェネレーターをロールする)

使用しない理由がありますという理由だけでANTLRをバイソンココ/ RGrammaticaJavaCCのレモンゆでSableCCQuexなどあなたが即座にあなた自身のパーサー+のレクサーをロールすべきであるという意味ではありません- 。

これらのツールがどれも十分ではない理由を特定てください-なぜあなたはあなたの目標を達成できないのですか?

扱っている文法の奇妙な部分が一意であることが確実でない限り、単一のカスタムパーサーとレクサーを作成するだけではいけません。代わりに、あなたが望むものを作成するツールを作成しますが、将来のニーズを満たすために使用することもできます。それを他の人があなたと同じ問題を抱えることを防ぐためにフリーソフトウェアとしてリリースします。


1
最初にパーサージェネレーターを試してからカスタムソリューションを試すことに同意しますが、具体的な(欠点)利点は何ですか?これはほぼ一般的なアドバイスです。
マニエロ

1
それは一般的なアドバイスです-しかし、あなたは一般的な質問をしました。:P明日、賛否両論に関するより具体的な考えを加えて拡張します。
ピーターボートン

1
カスタムパーサーとレクサーの作成に必要な作業量を過小評価していると思います。特に再利用可能なもの。

8

独自のパーサーをローリングすると、言語の複雑さを直接考えるようになります。言語を解析するのが難しい場合、おそらく理解するのは難しいでしょう。

初期のパーサージェネレーターには、非常に複雑な(「拷問された」と言われる)言語構文に動機付けられて多くの関心が寄せられていました。JOVIALは特に悪い例でした。他のすべてが最大で1つのシンボルを必要としていたときに、2つのシンボルを先読みする必要がありました。これにより、JOVIALコンパイラーのパーサーの生成が予想よりも困難になりました(General Dynamics / Fort Worth DivisionがF-16プログラム用のJOVIALコンパイラーを調達したときに困難な方法を学んだため)。

現在、コンパイラー作成者にとっては再帰的降下がより簡単であるため、再帰降下が普遍的に推奨される方法です。再帰下降コンパイラは、複雑で煩雑な言語よりも単純でクリーンな言語の再帰下降パーサーを書く方がはるかに簡単であるという点で、シンプルでクリーンな言語設計に強く報います。

最後に、あなたの言語をLISPに埋め込み、LISPインタプリタに手間をかけてもらうことを検討しましたか?AutoCADはそれを行い、それが彼らの生活をずっと楽にしてくれることを発見しました。かなりの数の軽量のLISPインタープリターがあり、一部は埋め込み可能です。


カスタムソリューションを展開することは興味深い議論です。
マニエロ

1
非常に素晴らしい。情報のポイントとして、FortranがJOVIALの前に、物事を解析するためにほぼ任意の(行全体)先読みを必要としていたことを追加します。しかし、当時、彼らは他の言語の作り方(または実装方法)を知りませんでした。
マクニール

移動は、移動するのが本当に価値があるかどうかを考える時間を与えてくれるので、交通機関の最良の手段です。それも健康的です。
babou 14年

6

私は一度商用アプリケーション用のパーサーを書いたことがあり、yaccを使用しました。開発者がC ++ですべてを手作業で記述し、約5倍遅く動作する競合するプロトタイプがありました。

このパーサーのレクサーについては、完全に手書きで書きました。約10年前だったので、正確には覚えていません-Cで約1000行かかりました。

字句解析器を手で書いた理由は、パーサーの入力文法でした。私が設計したものとは対照的に、それは要件であり、私のパーサーの実装が準拠しなければならないものでした。(もちろん、私はそれを別の方法で設計したでしょう。そして、もっと良い!)文法は、コンテキストに大きく依存し、レキシングさえ、いくつかの場所のセマンティクスに依存していました。たとえば、セミコロンは、ある場所ではトークンの一部であるが、別の場所ではセパレーターになる可能性があります。これは、以前に解析された一部の要素の意味解釈に基づいています。そのため、私は手書きの字句解析プログラムにそのようなセマンティックな依存関係を「埋め」、それによってyaccに実装しやすいかなり簡単なBNFを残しました。

マクニールに対応して追加されたもの:yaccは、プログラマーが端末、非端末、プロダクションなどの観点から考えることができる非常に強力な抽象化を提供します。また、yylex()関数を実装するときに、現在のトークンを返すことに集中することができ、その前後に何があるか心配する必要がなくなりました。C ++プログラマーは、このような抽象化の恩恵を受けずに文字レベルで作業し、最終的にはより複雑で効率の悪いアルゴリズムを作成しました。遅い速度は、C ++自体やライブラリとは関係ないと結論付けました。ファイルをメモリにロードして、純粋な解析速度を測定しました。ファイルバッファリングの問題が発生した場合、yaccはそれを解決するための最適なツールではありません。

また、追加したい:これはパーサー全般を記述するためのレシピではなく、ある特定の状況でどのように機能するかの例にすぎません。


私は手で5倍遅いC ++実装に興味があります:おそらくそれは貧弱なファイルバッファリングでしたか?それは大きな違いを生むことができます。
マクニール

@Macneil:回答に追加を投稿します。コメントが長すぎます。
アジェグロフ

1
++良い経験。パフォーマンスを重視しすぎません。愚かな不必要なものによって、それ以外の点では優れたプログラムが遅くなるのは簡単です。何をしてはいけないかを知るのに十分な再帰下降パーサーを作成したので、もっと高速なものがあるかどうかは疑問です。結局のところ、文字を読む必要があります。テーブルから実行されるパーサーは少し遅くなると思いますが、おそらく気付くのに十分ではありません。
マイクダンラベイ

3

それは完全に解析する必要があるものに依存します。レクサーの学習曲線に到達するよりも速く自分でロールバックできますか?後で決定を後悔しないように、静的に解析されるものは十分ですか?既存の実装が過度に複雑だと思いますか?もしそうなら、学習曲線をダッキングしていない場合にのみ、自分で転がして楽しんでください。

最近、私はレモンパーサーが本当に好きになりました。これは間違いなく、これまで使った中で最もシンプルで簡単なものです。物事を維持しやすくするために、私はそれをほとんどのニーズに使用しています。SQLiteは、他の注目すべきプロジェクトと同様にそれを使用します。

しかし、私はレクサーにはまったく興味がありません。あなたはそうかもしれません、もしそうなら、なぜそれを作ってみませんか?私はあなたが存在するものを使用することに戻ってくると感じていますが、必要であればかゆみを掻いてください:)


3
+1「レクサーの学習曲線に到達するよりも速く自分のロールを回せますか?」
ボバ

ええ、良い点。
マニエロ

3

それはあなたの目標が何であるかによります。

パーサー/コンパイラーがどのように機能するかを学習しようとしていますか?次に、独自にゼロから作成します。それが、彼らがしていることのすべてを理解することを本当に学ぶ唯一の方法です。私は過去数ヶ月間、これを書いてきましたが、特に「ああ、だからこそ、言語Xがこれを行うのは...」という瞬間であり、興味深い貴重な経験でした。

締め切りの申請のために何かを素早くまとめる必要がありますか?その後、おそらくパーサーツールを使用します。

次の10、20、おそらく30年で拡張したいものが必要ですか?自分で書き、時間をかけてください。それだけの価値があるでしょう。


それはコンパイラに関する私の最初の仕事であり、私は学習/実験しており、それを長い間維持するつもりです。
マニエロ

3

Martin Fowlersの言語ワークベンチアプローチを検討しましたか?記事から引用

言語ワークベンチが方程式に加える最も明らかな変更は、外部DSLの作成の容易さです。パーサーを書く必要はもうありません。抽象構文を定義する必要がありますが、実際には非常に簡単なデータモデリング手順です。さらに、DSLは強力なIDEを取得しますが、そのエディターを定義するのに時間をかける必要があります。ジェネレーターはまだあなたがしなければならないものであり、私の感覚ではそれはこれまで以上に簡単ではないということです。ただし、適切でシンプルなDSL用のジェネレーターを構築することは、この演習の最も簡単な部分の1つです。

それを読んで、私はあなた自身のパーサーを書く日々が終わったと言うでしょうそして利用可能なライブラリの1つを使うほうが良いです。ライブラリを習得すると、将来作成するすべてのDSLがその知識から恩恵を受けます。また、他の人は構文解析に対するあなたのアプローチを学ぶ必要はありません。

コメント(および修正された質問)をカバーするために編集

独自のローリングの利点

  1. あなたはパーサーを所有し、複雑な一連の問題を通して思考のすべての素敵な経験を得るでしょう
  2. 他の誰も考えていない特別な何かを思いつくかもしれません(そうではありませんが、賢い人のようです)
  3. 興味深い問題に悩まされることはありません

要するに、マスターすることに強く動機付けられていると感じる深刻な困難な問題の腸の奥深くまで本当にハックしたいときは、自分でロールバックするべきです。

他の人のライブラリを使用する利点

  1. あなたは車輪の再発明を避けるでしょう(あなたが同意するプログラミングの一般的な問題)
  2. 最終結果(新しい言語)に集中でき、解析方法などについてあまり心配する必要はありません。
  3. 言語の動作がずっと速く表示されます(ただし、あなただけではないので報酬は少なくなります)

したがって、クイックエンドの結果が必要な場合は、他の人のライブラリを使用してください。

全体として、これは問題をどの程度所有したいか、したがってソリューションを選択することになります。あなたがそれをすべて望むなら、あなた自身を転がしてください。


それは思考に代わる素晴らしい選択肢です。
マニエロ

1
@bigownあなたの質問によりよく答えるように編集されました
ゲイリーロウ

2

自分で書くことの大きな利点は、自分で書く方法を知っていることです。yaccのようなツールを使用する大きな利点は、ツールの使用方法がわかることです。私は最初の探検のツリートップのファンです。


特に役に立たない。「運転を学ぶことの利点は、運転できることです。自転車に乗ることを学ぶことの利点は、自転車に乗ることができることです。」
Zearin

1

オープンソースのパーサージェネレーターをフォークして、独自に作成してみませんか?パーサージェネレーターを使用しない場合、言語の構文を大幅に変更すると、コードの保守が非常に難しくなります。

パーサーでは、正規表現(つまり、Perlスタイル)を使用してトークン化し、便利な関数を使用してコードを読みやすくしました。ただし、パーサーで生成されたコードは、ステートテーブルとlong switch- casesを作成することにより高速化でき.gitignoreます。

カスタムパーサーの2つの例を次に示します。

https://github.com/SHiNKiROU/DesignScript-基本的な方言。配列表記で先読みを書くのが面倒だったため、エラーメッセージの品質を犠牲にしました https://github.com/SHiNKiROU/ExprParser-数式計算機。奇妙なメタプログラミングのトリックに注意してください


0

「この実証済みの「ホイール」を使用するか、再発明する必要がありますか?」


1
この「ホイール」とは何ですか?;-)
ジェイソンホワイトホーン

IMOこれは、この質問に関する良い意見ではありません。これは特定のケースに適さない一般的なアドバイスです。私は、area51.stackexchange.com / proposals / 7848の提案が時期尚早に閉じられたのではないかと疑い始めました。
マニエロ

2
車輪が再発明されなかった場合、私たちは毎日100kmph +で旅行することはありません-木製の車軸で回転する大きな重い塊を提案しない限り、現代のタイヤの多くのバリエーションよりも優れていますたくさんの車?
ピーターボートン

それは妥当な意見であり、正しい直観です。この種のことは状況に完全に依存しているため、特定の利点または欠点を列挙できれば、この答えがより役立つと考えています。
マクニール

@Peter:何かを再発明することは1つのことです(まったく異なる方法で行われることを意味します)が、追加の要件を満たすために既存のソリューションを改良する方が優れています。私はすべて「改善」に取り組んでいますが、すでに解決された問題について図面に戻るのは間違っているようです。
JBRウィルキンソン
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.