私は作成したマークアップ言語のパーサーを書いています(Pythonで書いていますが、これはこの質問にはあまり関係ありません。実際、これが悪い考えのように思える場合は、より良いパスの提案が欲しいです) 。
私はここでパーサーについて読んでいます: http://www.ferg.org/parsing/index.html 、そして私が正しく理解している場合は、コンテンツをトークンに変換します。私が理解できないのは、どのトークンタイプを使用するか、またはそれらを作成する方法です。たとえば、リンクした例のトークンタイプは次のとおりです。
私が抱えている問題は、より一般的なトークンの種類が私には少し恣意的に見えることです。たとえば、なぜSTRINGはIDENTIFIERに対して独自のトークンタイプなのですか。文字列は、STRING_START +(IDENTIFIER | WHITESPACE)+ STRING_STARTとして表すことができます。
これは私の言語の難しさと関係があるかもしれません。たとえば、変数宣言は{var-name var value}
として記述され、{var-name}
でデプロイされます。 '{'
と'}'
は独自のトークンである必要がありますが、VAR_NAMEとVAR_VALUEのトークンタイプであるか、または両方ともIDENTIFIERに該当しますか?さらに、VAR_VALUEには実際に空白を含めることができます。 var-name
の後の空白は、宣言内の値の開始を示すために使用されます。その他の空白は、値の一部です。この空白は独自のトークンになりますか?空白は、このコンテキストでのみその意味を持ちます。さらに、{
は変数宣言の開始ではない可能性があります。コンテキストによって異なります(Wordが再びあります!)。 {:
は名前宣言を開始し、{
は値の一部として使用することもできます。
私の言語はPythonブロックがインデントで作成されるという点で似ています。Pythonがレクサーを使用してINDENTトークンとDEDENTトークンを作成する方法(他の多くの言語で{
と}
が行うこととほぼ同じです)Pythonは文脈自由であると主張しています。つまり、少なくともレクサーは、トークンの作成時にストリーム内のどこにあるかを気にする必要はありません。Pythonのレクサーは、前の文字を認識せずに特定の長さのINDENTトークンを構築していることをどのように認識しますか(たとえば、前の行が改行だったため、スペースの作成を開始します) for INDENT)?私もこれを知る必要があるので尋ねます。
私の最後の質問は最も愚かなものです:なぜ字句解析器が必要なのですか?パーサーは文字ごとに進み、それがどこにあり、何を期待しているのかを理解できるように思えます。レクサーは単純さの利点を追加しますか?
(最後の段落のヒントとしての)あなたの質問は、実際にはレクサーに関するものではなく、レクサーとパーサーの間のインターフェースの正しい設計に関するものです。ご想像のとおり、レクサーとパーサーの設計に関する本はたくさんあります。私はたまたま Dick Gruneによるパーサー本 が好きですが、それは良い入門書ではないかもしれません。私は AppelによるCベースの本 を非常に嫌いです。なぜなら、コードはあなた自身のコンパイラに有効に拡張できないためです(Cのふりをするという決定に内在するメモリ管理の問題がMLに似ているため)。私自身の紹介は PJブラウンの本 でしたが、一般的な紹介としては適切ではありません(特に通訳には非常に適しています)。しかし、あなたの質問に戻りましょう。
正解は、前方参照制約または後方参照制約を使用する必要なく、レクサーでできる限りのことを行うことです。
これは(もちろん言語の詳細に応じて)文字列を "文字の後にnot-のシーケンスが続き、さらに"の文字として認識する必要があることを意味します。これを単一のユニットとしてパーサーに返します。いくつかあります。これには理由がありますが、重要なのは
多くの場合、パーサーはレクサーからトークンを受け取るとすぐにアクションを実行できます。たとえば、IDENTIFIERを受信するとすぐに、パーサーはシンボルテーブルのルックアップを実行して、シンボルがすでに知られているかどうかを確認できます。パーサーが文字列定数をQUOTE(IDENTIFIER SPACES)* QUOTEとしても解析する場合、無関係なシンボルテーブルのルックアップを多数実行するか、構文テーブルの構文要素のツリーの上位にシンボルテーブルのルックアップを積み上げることになります。文字列を見ていないことが確実になった時点で、.
私が言おうとしていることを言い換えると、別の言い方をすれば、レクサーは物事のスペルに、パーサーは物事の構造に関心を持つべきです。
文字列がどのように見えるかの説明が正規表現によく似ていることに気付くでしょう。これは偶然ではありません。字句解析器は 小さな言語 で頻繁に実装されます(- Jon Bentleyの優れたProgramming Pearls本の意味で ) =)正規表現を使用します。私はテキストを認識するとき、正規表現で考えることに慣れているだけです。
空白についての質問については、レクサーで認識してください。言語がかなり自由な形式であることが意図されている場合、WHITESPACEトークンをパーサーに返さないでください。それはそれらを捨てるだけでよいので、パーサーのプロダクションルールは本質的にノイズでスパムされます-投げるために認識することそれらを離れて。
空白が構文的に重要である場合の空白の処理方法についてそれが何を意味するかについては、あなたの言語についての知識がなくても本当にうまく機能するかどうかを判断できるとは思いません。私のスナップの判断は、空白が時々重要である場合とそうでない場合とを避け、ある種の区切り文字(引用符など)を使用することです。ただし、言語を好きなように設計できない場合は、このオプションを使用できない場合があります。
言語解析システムを設計する方法は他にもあります。確かに、レクサーとパーサーを組み合わせたシステムを指定できるコンパイラ構築システムがあります(Java [〜#〜] antlr [〜#〜]のバージョンと思います =これを行います)しかし、私はこれを使用したことがありません。
最後に歴史的なメモ。数十年前は、2つのプログラムが同時にメモリに収まらないため、パーサーに渡す前に、字句解析プログラムができる限り多くを行うことが重要でした。レクサーでより多くのことを行うと、より多くのメモリが利用可能になり、パーサーがスマートになります。私はWhitesmiths Cコンパイラを何年も使用してきましたが、私が正しく理解していれば、 64KBのRAM(これは小さなモデルのMS-DOSプログラムでした)、それでもANSI Cに非常に近いCのバリアントを変換しました。
私はあなたの最後の質問をします、それは実際、ばかではありません。パーサーは、文字ごとに複雑な構造を構築できます。思い出すと、ハービソンアンドスティールの文法(「C-リファレンスマニュアル」)には、終端として単一文字を使用し、単一文字から非終端として識別子、文字列、数値などを作成するプロダクションがあります。
形式言語の観点からは、正規表現ベースのレクサーが認識して「文字列リテラル」、「識別子」、「番号」、「キーワード」などとして分類できるものはすべて、LL(1)パーサーでも認識できます。したがって、パーサージェネレーターを使用してすべてを認識することには理論的な問題はありません。
アルゴリズムの観点からは、正規表現認識機能は、どのパーサーよりもはるかに高速に実行できます。認識の観点からは、プログラマが正規表現レクサーとパーサージェネレーターで書かれたパーサーの間の作業を分割する方がおそらく簡単です。
実際の考慮事項により、人々は別々のレクサーとパーサーを持つことを決定するようになると思います。
文法を本当に理解せずにレクサー/パーサーを作成しようとしているようです。通常、人々はレクサーとパーサーを書いているとき、それらをある文法に準拠するように書いています。 パーサーがこれらのトークンを使用してルール/非端末に一致する一方で、レクサーは文法内のトークンを返す必要があります。バイト単位で入力を簡単に解析できる場合、レクサーとパーサーはやりすぎかもしれません。
レクサーは物事をより簡単にします。
文法の概要:文法は、構文または入力がどのように見えるかに関する一連の規則です。たとえば、おもちゃの文法は次のとおりです(simple_commandは開始記号です)。
simple_command:
Word DIGIT AND_SYMBOL
simple_command:
addition_expression
addition_expression:
NUM '+' NUM
この文法は、
simple_commandは、
A)Word、DIGIT、AND_SYMBOL(これらは私が定義する「トークン」)
B)「addition_expression」(これはルールまたは「非終端」です)
加算式は以下で構成されます。
NUMの後に '+'の後にNUMが続きます(NUMは私が定義する「トークン」であり、 '+'はリテラルのプラス記号です)。
したがって、simple_commandは「開始記号」(私が開始する場所)なので、トークンを受け取ったら、それがsimple_commandに適合するかどうかを確認します。入力の最初のトークンがWordで、次のトークンがDIGITで、次のトークンがAND_SYMBOLである場合、いくつかのsimple_commandに一致しているため、何らかのアクションを実行できます。そうでない場合は、simple_commandの他のルールであるadditional_expressionと一致させようとします。したがって、最初のトークンがNUMの後に「+」が続き、その後にNUMが続いている場合は、simple_commandに一致し、何らかのアクションを実行します。これらのどちらでもない場合、構文エラーがあります。
これは非常に基本的な文法の紹介です。より完全に理解するには、 このwikiの記事 をチェックして、コンテキストフリーの文法チュートリアルをWebで検索してください。
レクサー/パーサーの配置を使用して、パーサーがどのように見えるかの例を次に示します。
bool simple_command(){
if (peek_next_token() == Word){
get_next_token();
if (get_next_token() == DIGIT){
if (get_next_token() == AND_SYMBOL){
return true;
}
}
}
else if (addition_expression()){
return true;
}
return false;
}
bool addition_expression(){
if (get_next_token() == NUM){
if (get_next_token() == '+'){
if (get_next_token() == NUM){
return true;
}
}
}
return false;
}
わかりましたので、そのコードは見苦しく、トリプルネストされたifステートメントはお勧めしません。しかし、要点はNiceモジュールの「get_next_token」関数と「peek_next_token」関数を使用する代わりに、文字ごとに上記のことを行うことを想像してみてください。真剣に、それを試してみてください。結果は気に入らないでしょう。上記の文法は、ほとんどすべての有用な文法よりも約30倍複雑ではないことに注意してください。レクサーを使用するメリットはありますか?
正直なところ、レクサーとパーサーは世界で最も基本的なトピックではありません。最初に文法について読んで理解し、次にレクサー/パーサーについて少し読んでから、詳しく説明することをお勧めします。
私の最後の質問は最も愚かなものです:なぜ字句解析器が必要なのですか?パーサーは文字ごとに進み、それがどこにあり、何を期待しているのかを理解できるように思えます。
これは愚かではなく、ただの真実です。
ただし、実用性は、ツールや目的によって多少異なります。たとえば、字句解析器なしでyaccを使用し、識別子にUnicode文字を許可する場合は、有効なすべての文字を明示的に列挙する大きくて醜い規則を記述する必要があります。一方、レクサーでは、文字が文字カテゴリのメンバーであるかどうかをライブラリルーチンに尋ねることができます。
レクサーを使用するかどうかは、言語と文字レベルの間の抽象化レベルの問題です。現在の文字レベルは、バイトレベルより上の別の抽象であり、ビットレベルより上の抽象であることに注意してください。
したがって、最後に、ビットレベルで解析することもできます。
STRING_START + (IDENTIFIER | WHITESPACE) + STRING_START.
いいえ、できません。 "("
?あなたによると、それは有効な文字列ではありません。そして脱出?
一般に、空白を処理する最良の方法は、トークンの区切りを超えて、空白を無視することです。多くの人々は非常に異なる空白を好んでおり、空白のルールを適用することはせいぜい物議を醸しています。