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