私はパーサーとパーサージェネレーターについて読んでいて、ウィキペディアのLR解析ページでこのステートメントを見つけました:
多くのプログラミング言語は、LRパーサーのいくつかのバリエーションを使用して解析できます。 1つの注目すべき例外はC++です。
なぜそうですか? C++のどの特定のプロパティがLRパーサーで解析できないようにしますか?
グーグルを使用すると、CはLR(1)で完全に解析できるが、C++ではLR(∞)が必要であることがわかりました。
Lambda the Ultimate に興味深いスレッドがあり、これは C++のLALR文法 について説明しています。
C++解析の説明を含む PhD論文 へのリンクが含まれています。
「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つの異なる解析があります。
さて、後者はバカだと思うかもしれませんが、無視すべきです。ほとんどがあなたに同意するでしょう。ただし、副作用がある場合があります(たとえば、multiplyがオーバーロードされている場合)。しかし、それはポイントではありません。ポイントがあるのはare2つの異なる解析であり、したがって、プログラムはこのshould解析されました。
コンパイラーは、適切な状況下で適切なものを受け入れなければならず、他の情報(たとえば、xのタイプの知識)がない場合は、後で何をすべきかを決定するために両方を収集する必要があります。したがって、文法ではこれを許可する必要があります。そして、それは文法を曖昧にします。
したがって、純粋なLR解析ではこれを処理できません。 Antlr、JavaCC、YACC、または従来のBisonなど、広く利用可能な他の多くのパーサージェネレーター、またはPEGスタイルのパーサーを「純粋な」方法で使用することもできません。
より複雑なケースがたくさんあります(テンプレート構文の解析には任意の先読みが必要ですが、LALR(k)は最大k個のトークンを先読みできます)が、撃ち落とすには反例が1つだけ必要ですpureLR(またはその他)の解析。
ほとんどの実際のC/C++パーサーは、ある種の決定論的なパーサーと追加のハックを使用してこの例を処理します。つまり、構文解析とシンボルテーブルコレクションが絡み合います。またはそうでないため、2つの潜在的な解析から選択できます。しかし、これを行うパーサーはコンテキストに依存せず、LRパーサー(純粋なパーサーなど)は(せいぜい)コンテキストに依存しません。
この曖昧さをなくすために、LRパーサーにチートし、ルールごとの削減時間のセマンティックチェックを追加できます。 (このコードはしばしば単純ではありません)。他のほとんどのパーサータイプには、構文解析のさまざまな時点でセマンティックチェックを追加する手段があり、これを使用してこれを行うことができます。
そして、あなたが十分にチートすれば、LRパーサーをCおよびC++で動作させることができます。 GCCの連中はしばらくしていたが、より良いエラー診断を望んでいたので、手作業でコード化された解析をあきらめた。
ただし、別のアプローチがあります。これは、ニースでクリーンであり、シンボルテーブルハッカーなしでCおよびC++を問題なく解析します。 GLRパーサー 。これらは完全なコンテキストフリーのパーサーです(実質的に無限の先読みを持ちます)。 GLRパーサーは、単純にboth解析を受け入れ、あいまいな解析を表す「ツリー」(実際にはほとんどツリーのような有向非循環グラフ)を生成します。解析後のパスにより、あいまいさを解決できます。
この手法は、DMS Software Reengineering TookitのCおよびC++フロントエンドで使用します(2017年6月現在、これらはMSで完全なC++ 17とGNUダイアレクト)を処理します。大規模なCおよびC++システムの数百万行を処理し、ソースコードの完全な詳細を含むASTを生成する完全かつ正確な解析を行います(C++の最も厄介な点については、 the AST解析します。 )
問題がこのように定義されることはありませんが、興味深いはずです:
この新しい文法を「コンテキストに依存しない」yaccパーサーで完全に解析できるようにするために必要な、C++文法の最小の修正セットは何ですか? (1つの「ハック」のみを使用する:型名/識別子の曖昧性解消、パーサーがすべてのtypedef/class/structをレクサーに通知する)
私はいくつかのものを見ます:
_Type Type;
_は禁止されています。型名として宣言された識別子は、非型名識別子になることはできません(_struct Type Type
_はあいまいではなく、引き続き許可されることに注意してください)。
_names tokens
_には3つのタイプがあります。
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
_ディレクティブの外側では禁止されます。これは、非常に多くのC++ヘッダーに存在する愚かな_sizeof int
_あいまいさへの大きな一歩となります。
再構成されたC++を実装するコンパイラは、あいまいな構文を使用するC++ソースに遭遇した場合、_source.cpp
_も_ambiguous_syntax
_フォルダーに移動し、それをコンパイルする前に明確な翻訳済み_source.cpp
_を自動的に作成します。
あいまいなC++構文を知っている場合は追加してください!
ここに答えてください でわかるように、C++には、操作の順序、したがって、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を作成した男です。)