回答:
本当に3つのオプションがあり、それらの3つはすべて異なる状況で望ましいものです。
たとえば、古代のデータ形式のパーサーを今すぐ構築するように求められます。または、パーサーを高速にする必要があります。または、パーサーを簡単に保守できる必要があります。
これらの場合、おそらくパーサージェネレーターを使用するのが最善です。詳細をいじる必要はありません。適切に機能するために多くの複雑なコードを取得する必要はありません。入力が順守する文法を書き、処理コードとpresto:インスタントパーサーを書くだけです。
利点は明らかです。
パーサージェネレーターで注意しなければならないことが1つあります:文法を拒否することがあります。さまざまなタイプのパーサーの概要と、それらがどのように噛み付くかについては、ここから始めてください。ここでは、多くの実装の概要と、それらが受け入れる文法の種類を見つけることができます。
パーサージェネレーターは便利ですが、あまりユーザー(エンドユーザーではなく、ユーザー)に優しいわけではありません。通常、適切なエラーメッセージを表示したり、エラー回復を提供したりすることはできません。おそらくあなたの言語は非常に奇妙であり、パーサーは文法を拒否するか、ジェネレーターが提供する以上の制御が必要です。
これらの場合、手書きの再帰下降パーサーを使用するのがおそらく最善です。正しく取得するのは複雑かもしれませんが、パーサーを完全に制御できるため、エラーメッセージやエラー回復など、パーサージェネレーターでは実行できないあらゆる種類の処理を実行できます(C#ファイルからすべてのセミコロンを削除してください:C#コンパイラは文句を言いますが、セミコロンの有無にかかわらず、とにかく他のほとんどのエラーを検出します)。
また、パーサーの品質が十分に高いと仮定すると、手書きパーサーは通常、生成されたパーサーよりもパフォーマンスが高くなります。一方で、優れたパーサーを作成できない場合(通常、経験、知識、または設計の不足(の組み合わせ)が原因)、パフォーマンスは通常遅くなります。ただし、レクサーの場合は逆です。通常、生成されたレクサーはテーブル検索を使用し、(ほとんどの)手書きのものよりも高速になります。
教育面では、独自のパーサーを作成すると、ジェネレーターを使用する以上のことがわかります。結局、ますます複雑なコードを書く必要があり、さらに言語の解析方法を正確に理解する必要があります。一方、独自の言語を作成する方法を学びたい場合(言語設計の経験を積む場合)、オプション1またはオプション3のいずれかをお勧めします。言語を開発している場合、おそらく大きく変わるでしょう。オプション1と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#ではクラスである必要があるため、ラッパークラスを導入する必要がありました)
上記のコードが非常に強力であることをご理解いただければ幸いです。思いつく文法はすべて解析できます。多くのタスクを実行できる任意のコードを文法に追加できます。これをすべて機能させることができれば、結果のコードを再利用して非常に簡単に多くのタスクを実行できます。このコードを使用してコマンドラインインタープリターを構築することを想像してください。
パーサーを作成したことがない場合は、これを行うことをお勧めします。それは楽しく、物事がどのように機能するかを学び、パーサーとレクサージェネレーターが次にパーサーを必要とするときの労力を節約できることを学びます。
また、http://compilers.iecc.com/crenshaw/を読むことをお勧めします。http://compilers.iecc.com/crenshaw/は、それを行う方法に対して非常に現実的な態度を持っているからです。
独自の再帰降下パーサーを作成する利点は、構文エラーに関する高品質のエラーメッセージを生成できることです。パーサージェネレーターを使用すると、特定の時点でエラーを生成し、カスタムエラーメッセージを追加できますが、パーサージェネレーターは、解析を完全に制御する能力とは一致しません。
独自に記述することのもう1つの利点は、文法と1対1の対応関係を持たない単純な表現に解析しやすいことです。
文法が修正されており、エラーメッセージが重要な場合は、独自のローリングを検討するか、少なくとも必要なエラーメッセージを提供するパーサージェネレーターの使用を検討してください。文法が常に変化している場合は、代わりにパーサージェネレーターの使用を検討する必要があります。
Bjarne Stroustrupは、C ++の最初の実装でYACCをどのように使用したかについて語っています(C ++ の設計と進化を参照)。その最初のケースでは、彼は代わりに彼自身の再帰降下パーサーを書くことを望みました!
オプション3:どちらでもない(独自のパーサージェネレーターをロールする)
使用しない理由がありますという理由だけでANTLRを、バイソン、ココ/ R、Grammatica、JavaCCの、レモン、ゆで、SableCC、Quex、などあなたが即座にあなた自身のパーサー+のレクサーをロールすべきであるという意味ではありません- 。
これらのツールがどれも十分ではない理由を特定してください-なぜあなたはあなたの目標を達成できないのですか?
扱っている文法の奇妙な部分が一意であることが確実でない限り、単一のカスタムパーサーとレクサーを作成するだけではいけません。代わりに、あなたが望むものを作成するツールを作成しますが、将来のニーズを満たすために使用することもできます。それを他の人があなたと同じ問題を抱えることを防ぐためにフリーソフトウェアとしてリリースします。
独自のパーサーをローリングすると、言語の複雑さを直接考えるようになります。言語を解析するのが難しい場合、おそらく理解するのは難しいでしょう。
初期のパーサージェネレーターには、非常に複雑な(「拷問された」と言われる)言語構文に動機付けられて多くの関心が寄せられていました。JOVIALは特に悪い例でした。他のすべてが最大で1つのシンボルを必要としていたときに、2つのシンボルを先読みする必要がありました。これにより、JOVIALコンパイラーのパーサーの生成が予想よりも困難になりました(General Dynamics / Fort Worth DivisionがF-16プログラム用のJOVIALコンパイラーを調達したときに困難な方法を学んだため)。
現在、コンパイラー作成者にとっては再帰的降下がより簡単であるため、再帰降下が普遍的に推奨される方法です。再帰下降コンパイラは、複雑で煩雑な言語よりも単純でクリーンな言語の再帰下降パーサーを書く方がはるかに簡単であるという点で、シンプルでクリーンな言語設計に強く報います。
最後に、あなたの言語をLISPに埋め込み、LISPインタプリタに手間をかけてもらうことを検討しましたか?AutoCADはそれを行い、それが彼らの生活をずっと楽にしてくれることを発見しました。かなりの数の軽量のLISPインタープリターがあり、一部は埋め込み可能です。
私は一度商用アプリケーション用のパーサーを書いたことがあり、yaccを使用しました。開発者がC ++ですべてを手作業で記述し、約5倍遅く動作する競合するプロトタイプがありました。
このパーサーのレクサーについては、完全に手書きで書きました。約10年前だったので、正確には覚えていません-Cで約1000行かかりました。
字句解析器を手で書いた理由は、パーサーの入力文法でした。私が設計したものとは対照的に、それは要件であり、私のパーサーの実装が準拠しなければならないものでした。(もちろん、私はそれを別の方法で設計したでしょう。そして、もっと良い!)文法は、コンテキストに大きく依存し、レキシングさえ、いくつかの場所のセマンティクスに依存していました。たとえば、セミコロンは、ある場所ではトークンの一部であるが、別の場所ではセパレーターになる可能性があります。これは、以前に解析された一部の要素の意味解釈に基づいています。そのため、私は手書きの字句解析プログラムにそのようなセマンティックな依存関係を「埋め」、それによってyaccに実装しやすいかなり簡単なBNFを残しました。
マクニールに対応して追加されたもの:yaccは、プログラマーが端末、非端末、プロダクションなどの観点から考えることができる非常に強力な抽象化を提供します。また、yylex()
関数を実装するときに、現在のトークンを返すことに集中することができ、その前後に何があるか心配する必要がなくなりました。C ++プログラマーは、このような抽象化の恩恵を受けずに文字レベルで作業し、最終的にはより複雑で効率の悪いアルゴリズムを作成しました。遅い速度は、C ++自体やライブラリとは関係ないと結論付けました。ファイルをメモリにロードして、純粋な解析速度を測定しました。ファイルバッファリングの問題が発生した場合、yaccはそれを解決するための最適なツールではありません。
また、追加したい:これはパーサー全般を記述するためのレシピではなく、ある特定の状況でどのように機能するかの例にすぎません。
それは完全に解析する必要があるものに依存します。レクサーの学習曲線に到達するよりも速く自分でロールバックできますか?後で決定を後悔しないように、静的に解析されるものは十分ですか?既存の実装が過度に複雑だと思いますか?もしそうなら、学習曲線をダッキングしていない場合にのみ、自分で転がして楽しんでください。
最近、私はレモンパーサーが本当に好きになりました。これは間違いなく、これまで使った中で最もシンプルで簡単なものです。物事を維持しやすくするために、私はそれをほとんどのニーズに使用しています。SQLiteは、他の注目すべきプロジェクトと同様にそれを使用します。
しかし、私はレクサーにはまったく興味がありません。あなたはそうかもしれません、もしそうなら、なぜそれを作ってみませんか?私はあなたが存在するものを使用することに戻ってくると感じていますが、必要であればかゆみを掻いてください:)
それはあなたの目標が何であるかによります。
パーサー/コンパイラーがどのように機能するかを学習しようとしていますか?次に、独自にゼロから作成します。それが、彼らがしていることのすべてを理解することを本当に学ぶ唯一の方法です。私は過去数ヶ月間、これを書いてきましたが、特に「ああ、だからこそ、言語Xがこれを行うのは...」という瞬間であり、興味深い貴重な経験でした。
締め切りの申請のために何かを素早くまとめる必要がありますか?その後、おそらくパーサーツールを使用します。
次の10、20、おそらく30年で拡張したいものが必要ですか?自分で書き、時間をかけてください。それだけの価値があるでしょう。
Martin Fowlersの言語ワークベンチアプローチを検討しましたか?記事から引用
言語ワークベンチが方程式に加える最も明らかな変更は、外部DSLの作成の容易さです。パーサーを書く必要はもうありません。抽象構文を定義する必要がありますが、実際には非常に簡単なデータモデリング手順です。さらに、DSLは強力なIDEを取得しますが、そのエディターを定義するのに時間をかける必要があります。ジェネレーターはまだあなたがしなければならないものであり、私の感覚ではそれはこれまで以上に簡単ではないということです。ただし、適切でシンプルなDSL用のジェネレーターを構築することは、この演習の最も簡単な部分の1つです。
それを読んで、私はあなた自身のパーサーを書く日々が終わったと言うでしょう、そして利用可能なライブラリの1つを使うほうが良いです。ライブラリを習得すると、将来作成するすべてのDSLがその知識から恩恵を受けます。また、他の人は構文解析に対するあなたのアプローチを学ぶ必要はありません。
コメント(および修正された質問)をカバーするために編集
独自のローリングの利点
要するに、マスターすることに強く動機付けられていると感じる深刻な困難な問題の腸の奥深くまで本当にハックしたいときは、自分でロールバックするべきです。
他の人のライブラリを使用する利点
したがって、クイックエンドの結果が必要な場合は、他の人のライブラリを使用してください。
全体として、これは問題をどの程度所有したいか、したがってソリューションを選択することになります。あなたがそれをすべて望むなら、あなた自身を転がしてください。
オープンソースのパーサージェネレーターをフォークして、独自に作成してみませんか?パーサージェネレーターを使用しない場合、言語の構文を大幅に変更すると、コードの保守が非常に難しくなります。
パーサーでは、正規表現(つまり、Perlスタイル)を使用してトークン化し、便利な関数を使用してコードを読みやすくしました。ただし、パーサーで生成されたコードは、ステートテーブルとlong switch
- case
sを作成することにより高速化でき.gitignore
ます。
カスタムパーサーの2つの例を次に示します。
https://github.com/SHiNKiROU/DesignScript-基本的な方言。配列表記で先読みを書くのが面倒だったため、エラーメッセージの品質を犠牲にしました https://github.com/SHiNKiROU/ExprParser-数式計算機。奇妙なメタプログラミングのトリックに注意してください
「この実証済みの「ホイール」を使用するか、再発明する必要がありますか?」