私はパーサーとパーサージェネレーターについて読んでいて、このステートメントをウィキペディアのLR解析ページで見つけました。
多くのプログラミング言語は、LRパーサーのバリエーションを使用して解析できます。注目すべき例外の1つはC ++です。
なぜそうなのですか?C ++の特定のプロパティにより、LRパーサーで解析できなくなりますか?
Googleを使用して、CはLR(1)で完全に解析できることがわかりましたが、C ++ではLR(∞)が必要です。
私はパーサーとパーサージェネレーターについて読んでいて、このステートメントをウィキペディアのLR解析ページで見つけました。
多くのプログラミング言語は、LRパーサーのバリエーションを使用して解析できます。注目すべき例外の1つはC ++です。
なぜそうなのですか?C ++の特定のプロパティにより、LRパーサーで解析できなくなりますか?
Googleを使用して、CはLR(1)で完全に解析できることがわかりましたが、C ++ではLR(∞)が必要です。
回答:
Lambda the Ultimateには、C ++のLALR文法について説明する興味深いスレッドがあります。
これには、C ++解析の説明を含む博士論文へのリンクが含まれています。
「C ++文法は曖昧で、コンテキストに依存し、潜在的にいくつかのあいまいさを解決するために無限の先読みが必要です。」
さらに、いくつかの例を示します(PDFの147ページを参照)。
例は次のとおりです。
int(x), y, *const z;
意味
int x;
int y;
int *const z;
と比較:
int(x), y, new int;
意味
(int(x)), (y), (new int));
(コンマ区切りの式)。
2つのトークンシーケンスの最初のサブシーケンスは同じですが、解析ツリーが異なります。これは、最後の要素に依存します。曖昧さをなくすトークンの前に、任意の数のトークンが存在する可能性があります。
LRパーサーは、仕様上、あいまいな文法規則を処理できません。(1970年代、アイデアが練られていた頃は、理論はより簡単になりました)。
CおよびC ++はどちらも次のステートメントを許可します。
x * y ;
2つの異なる解析があります。
今、あなたは後者が愚かであり、無視されるべきだと思うかもしれません。ほとんどがあなたに同意します。ただし、副作用がある場合もあります(たとえば、乗算が過負荷の場合)。しかし、それは重要ではありません。ポイントがあります二つの異なる構文解析は、そのプログラムがどのようにこれに応じて、異なるものを意味することができますSHOULD解析されてきました。
コンパイラは、適切な状況下で適切なものを受け入れる必要があり、他に情報がない場合(たとえば、xのタイプに関する知識)は、後で何をするかを決定するために両方を収集する必要があります。したがって、文法はこれを許可する必要があります。そして、それは文法を曖昧にします。
したがって、純粋なLR解析ではこれを処理できません。「純粋な」方法で使用される、Antlr、JavaCC、YACC、または従来のBisonなどの広く利用可能な他の多くのパーサージェネレーター、あるいはPEGスタイルのパーサーであることもできません。
より複雑なケースはたくさんあります(テンプレート構文の解析には任意の先読みが必要ですが、LALR(k)は最大でk個のトークンを先読みできます)が、純粋に撃墜するための反例は1つだけです LR(または他の)解析です。
ほとんどの実際のC / C ++パーサーは、追加のハックを備えたある種の確定的パーサーを使用してこの例を処理します。これらは、シンボルテーブルコレクションと構文解析を絡み合わせています。そのため、「x」が検出されると、パーサーはxが型かどうかを認識します。かどうか、したがって、2つの潜在的な解析から選択できます。ただし、これを行うパーサーはコンテキストフリーではなく、LRパーサー(純粋なパーサーなど)は(せいぜい)コンテキストフリーです。
この曖昧さをなくすために、LRパーサーにチートを行い、ルールごとの削減時間セマンティックチェックを追加できます。(多くの場合、このコードは単純ではありません)。他のほとんどのタイプのパーサーには、構文解析のさまざまなポイントでセマンティックチェックを追加する手段があり、これを使用することができます。
そして、あなたが十分にごまかしている場合は、LRパーサーをCおよびC ++で動作させることができます。しばらくの間、GCCの人たちはそうしていましたが、手作業でコード化された解析のためにそれをあきらめました。
ただし、別のアプローチがあります。これは、素晴らしく、クリーンで、CおよびC ++を、シンボルテーブルハッカーなしで問題なく解析できます。GLR パーサーです。これらは、完全なコンテキストフリーパーサーです(事実上無限の先読みを備えています)。GLRパーサーは単に両方を受け入れます解析を、あいまいな解析を表す「ツリー」(実際にはほとんどツリーのような有向非循環グラフ)を生成します。解析後のパスは、あいまいさを解決できます。
DMSソフトウェアリエンジニアリングTookitのCおよびC ++フロントエンドでこの手法を使用しています(2017年6月現在、これらはMSおよびGNU方言で完全なC ++ 17を処理しています)。それらは、大規模なCおよびC ++システムの数百万行を処理するために使用されており、完全で正確な解析により、ソースコードの完全な詳細を含むASTが生成されます。(C ++の最も厄介な解析については、ASTを参照してください。)
x * y
、くすくす笑いました-このような気の利いた小さなあいまいさを誰もがどのように考えているかは驚くべきことです。
問題は、このように定義されることはありませんが、興味深いはずです。
この新しい文法を「非コンテキストフリー」のyaccパーサーで完全に解析できるようにするために必要となる、C ++文法に対する変更の最小セットは何ですか?(1つの 'hack'のみを使用:タイプ名/識別子の明確化、パーサーはすべてのtypedef / class / structのレクサーに通知します)
私はいくつかのものを見ます:
Type Type;
禁止されています。タイプ名として宣言された識別子は、タイプ名以外の識別子になることはできません(struct Type Type
あいまいではなく、引き続き許可される可能性があることに注意してください)。
次の3つのタイプがありますnames tokens
。
types
:組み込み型またはtypedef / class / structのためテンプレート関数を別のトークンと見なすことで、func<
あいまいさを解決できます。func
がテンプレート関数名の場合は、テンプレート<
パラメータリストの先頭でなければなりません。それ以外の場合func
は、関数ポインタで<
あり、比較演算子です。
Type a(2);
オブジェクトのインスタンス化です。
Type a();
そして、Type a(int)
関数のプロトタイプです。
int (k);
完全に禁じられています、書かれるべきです int k;
typedef int func_type();
そして
typedef int (func_type)();
禁止されています。
関数typedefは関数ポインターtypedefでなければなりません: typedef int (*func_ptr_type)();
テンプレートの再帰は1024に制限されています。それ以外の場合は、オプションの最大値をコンパイラに渡すことができます。
int a,b,c[9],*d,(*f)(), (*g)()[9], h(char);
禁止することもできます。 int a,b,c[9],*d;
int (*f)();
int (*g)()[9];
int h(char);
関数プロトタイプまたは関数ポインタ宣言ごとに1行。
非常に好ましい代替手段は、ひどい関数ポインタ構文を変更することです。
int (MyClass::*MethodPtr)(char*);
次のように再構文化されます:
int (MyClass::*)(char*) MethodPtr;
これはキャスト演算子と一貫しています (int (MyClass::*)(char*))
typedef int type, *type_ptr;
禁止することもできます:typedefごとに1行。したがって、それは
typedef int type;
typedef int *type_ptr;
sizeof int
、sizeof char
、sizeof long long
およびCO。各ソースファイルで宣言できます。このように、タイプを利用して、各ソースファイルがint
で始まる必要があります
#type int : signed_integer(4)
そしてunsigned_integer(4)
その外に禁止されるだろう#type
、これは愚かに大きな一歩となりディレクティブsizeof int
非常に多くのC ++ヘッダーに曖昧さの存在を
再構文化されたC ++を実装するコンパイラーは、あいまいな構文を使用するC ++ソースに遭遇した場合source.cpp
、ambiguous_syntax
フォルダーも移動し、source.cpp
それをコンパイルする前にあいまいでない翻訳を自動的に作成します。
何かがあれば、あいまいなC ++構文を追加してください。
ここの私の回答でわかるように、C ++には、型解決ステージ(通常、事後解析)が操作の順序を変更するため、LLまたはLRパーサーで確定的に解析できない構文が含まれているため、ASTの基本的な形状(通常、第1段階の解析によって提供されると予想されます)。
あなたは答えにかなり近いと思います。
LR(1)は、左から右への解析がコンテキストの先読みに1つのトークンのみを必要とすることを意味しますが、LR(∞)は、無限の先読みを意味します。つまり、パーサーは、それが現在どこにあるのかを理解するために、これから来るすべてを知る必要があります。
C ++の「typedef」問題は、構文解析中にシンボルテーブルを構築するLALR(1)パーサーで解析できます(純粋なLALRパーサーではありません)。「テンプレート」の問題は、おそらくこの方法では解決できません。この種類のLALR(1)パーサーの利点は、文法(以下に示す)がLALR(1)文法(あいまいさがない)であることです。
/* C Typedef Solution. */
/* Terminal Declarations. */
<identifier> => lookup(); /* Symbol table lookup. */
/* Rules. */
Goal -> [Declaration]... <eof> +> goal_
Declaration -> Type... VarList ';' +> decl_
-> typedef Type... TypeVarList ';' +> typedecl_
VarList -> Var /','...
TypeVarList -> TypeVar /','...
Var -> [Ptr]... Identifier
TypeVar -> [Ptr]... TypeIdentifier
Identifier -> <identifier> +> identifier_(1)
TypeIdentifier -> <identifier> =+> typedefidentifier_(1,{typedef})
// The above line will assign {typedef} to the <identifier>,
// because {typedef} is the second argument of the action typeidentifier_().
// This handles the context-sensitive feature of the C++ language.
Ptr -> '*' +> ptr_
Type -> char +> type_(1)
-> int +> type_(1)
-> short +> type_(1)
-> unsigned +> type_(1)
-> {typedef} +> type_(1)
/* End Of Grammar. */
次の入力は問題なく解析できます。
typedef int x;
x * y;
typedef unsigned int uint, *uintptr;
uint a, b, c;
uintptr p, q, r;
LRSTARパーサジェネレータは、上記文法表記を読み取り、パーサを生成するハンドル解析ツリーまたはASTにおける曖昧さ無し「のtypedef」問題。(開示:LRSTARを作成したのは私です)。