C ++がLR(1)パーサーで解析できないのはなぜですか?


153

私はパーサーとパーサージェネレーターについて読んでいて、このステートメントをウィキペディアのLR解析ページで見つけました。

多くのプログラミング言語は、LRパーサーのバリエーションを使用して解析できます。注目すべき例外の1つはC ++です。

なぜそうなのですか?C ++の特定のプロパティにより、LRパーサーで解析できなくなりますか?

Googleを使用して、CはLR(1)で完全に解析できることがわかりましたが、C ++ではLR(∞)が必要です。


7
同様に、再帰を学ぶには再帰を理解する必要があります;-)。
Toon Krijthe、2008年

5
「このフレーズを解析すると、パーサーを理解できるようになります。」
ilya n。

回答:


92

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つのトークンシーケンスの最初のサブシーケンスは同じですが、解析ツリーが異なります。これは、最後の要素に依存します。曖昧さをなくすトークンの前に、任意の数のトークンが存在する可能性があります。


29
このページの147ページについての要約があると便利です。私はそのページを読むつもりです。(+1)
Cheery

11
例は次のとおりです。int(x)、y、* const z; //意味:int x; int y; int * const z; (一連の宣言)int(x)、y、new int; //意味:(int(x))、(y)、(new int)); (コンマ区切りの式)2つのトークンシーケンスは、最初のサブシーケンスは同じですが、解析ツリーが異なります。これは、最後の要素に依存します。曖昧さをなくすトークンの前に、任意の数のトークンが存在する可能性があります。
Blaisorblade、2012年

6
まあ、その文脈では、先読みは常に入力の長さによって制限されるため、∞は「任意に多く」を意味します。
MauganRa 2014年

1
博士論文から引用された引用には戸惑っています。あいまいさがある場合、定義により、先読みはあいまいさを「解決」することはありません(つまり、少なくとも2つの解析が文法によって正しいと見なされるため、どの解析が正しいoenであるかを決定します)。さらに、引用はCのあいまいさについて言及していますが、説明ではあいまいさは示されていませんが、構文解析の決定は任意の長い先読みの後にのみ行うことができる曖昧でない例にすぎません。
dodecaplex

231

LRパーサーは、仕様上、あいまいな文法規則を処理できません。(1970年代、アイデアが練られていた頃は、理論はより簡単になりました)。

CおよびC ++はどちらも次のステートメントを許可します。

x * y ;

2つの異なる解析があります。

  1. タイプxへのポインターとして、yの宣言にすることができます。
  2. それはxとyの乗算であり、答えを捨てます。

今、あなたは後者が愚かであり、無視されるべきだと思うかもしれません。ほとんどがあなたに同意します。ただし、副作用がある場合もあります(たとえば、乗算が過負荷の場合)。しかし、それは重要ではありません。ポイントがあります二つの異なる構文解析は、そのプログラムがどのようにこれに応じて、異なるものを意味することができます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を参照してください


11
'x * y'の例は興味深いものですが、Cでも同じことが起こります( 'y'はtypedefまたは変数です)。しかし、CはLR(1)パーサーで解析できるので、C ++との違いは何ですか?
Martin Cote、

12
私の回答者は、Cにも同じ問題があることをすでに観察していました。いいえ、同じ理由で、LR(1)で解析することはできません。えー、 'y'がtypedefになれるってどういう意味?たぶん、あなたは「x」を意味しましたか?それは何も変わりません。
Ira Baxter、

6
*が副作用をもたらすようにオーバーライドされる可能性があるため、C ++では解析2は必ずしも愚かではありません。
Dour High Arch

8
私は見てx * y、くすくす笑いました-このような気の利いた小さなあいまいさを誰もがどのように考えているかは驚くべきことです。
new123456 2011年

51
@altie確かに、ビットシフト演算子をオーバーロードしてほとんどの変数型をストリームに書き込むことはできませんよね?
Troy Daniels

16

問題は、このように定義されることはありませんが、興味深いはずです。

この新しい文法を「非コンテキストフリー」のyaccパーサーで完全に解析できるようにするために必要となる、C ++文法に対する変更の最小セットは何ですか?(1つの 'hack'のみを使用:タイプ名/識別子の明確化、パーサーはすべてのtypedef / class / structのレクサーに通知します)

私はいくつかのものを見ます:

  1. Type Type;禁止されています。タイプ名として宣言された識別子は、タイプ名以外の識別子になることはできません(struct Type Typeあいまいではなく、引き続き許可される可能性があることに注意してください)。

    次の3つのタイプがありますnames tokens

    • types :組み込み型またはtypedef / class / structのため
    • テンプレート関数
    • 識別子:関数/メソッドと変数/オブジェクト

    テンプレート関数を別のトークンと見なすことで、func<あいまいさを解決できます。funcがテンプレート関数名の場合は、テンプレート<パラメータリストの先頭でなければなりません。それ以外の場合funcは、関数ポインタで<あり、比較演算子です。

  2. Type a(2);オブジェクトのインスタンス化です。 Type a();そして、Type a(int)関数のプロトタイプです。

  3. int (k); 完全に禁じられています、書かれるべきです int k;

  4. typedef int func_type(); そして typedef int (func_type)();禁止されています。

    関数typedefは関数ポインターtypedefでなければなりません: typedef int (*func_ptr_type)();

  5. テンプレートの再帰は1024に制限されています。それ以外の場合は、オプションの最大値をコンパイラに渡すことができます。

  6. 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*))

  7. typedef int type, *type_ptr; 禁止することもできます:typedefごとに1行。したがって、それは

    typedef int type;

    typedef int *type_ptr;

  8. sizeof intsizeof charsizeof long longおよびCO。各ソースファイルで宣言できます。このように、タイプを利用して、各ソースファイルがintで始まる必要があります

    #type int : signed_integer(4)

    そしてunsigned_integer(4)その外に禁止されるだろう#type 、これは愚かに大きな一歩となりディレクティブsizeof int非常に多くのC ++ヘッダーに曖昧さの存在を

再構文化されたC ++を実装するコンパイラーは、あいまいな構文を使用するC ++ソースに遭遇した場合source.cppambiguous_syntaxフォルダーも移動し、source.cppそれをコンパイルする前にあいまいでない翻訳を自動的に作成します。

何かがあれば、あいまいなC ++構文を追加してください。


3
C ++は十分に定着しています。実際には誰もこれをしません。フロントエンドを構築する(私たちのような)人々は、単に弾丸を噛み、パーサーを機能させるためのエンジニアリングを行います。そして、テンプレートがその言語で存在している限り、あなたは純粋な文脈自由なパーサーを取得するつもりはありません。
Ira Baxter

9

ここの私の回答でわかるように、C ++には、型解決ステージ(通常、事後解析)が操作順序を変更するため、LLまたはLRパーサーで確定的に解析できない構文が含まれているため、ASTの基本的な形状(通常、第1段階の解析によって提供されると予想されます)。


3
あいまいさを処理する解析技術は、解析時に両方の ASTバリアントを生成し、型情報に応じて誤ったものを単に排除します。
Ira Baxter

@Ira:はい、そうです。これの特別な利点は、第1段階の解析の分離を維持できることです。これはGLRパーサーで最も一般的に知られていますが、 "GLL?"を使用してC ++をヒットできなかったことがわかる特別な理由はありません。パーサーも。
サムハーウェル

「GLL」?まあ、確かに、しかしあなたは理論を理解し、残りを使うために論文を書かなければならないでしょう。より可能性が高いのは、トップダウンハンドでコーディングされたパーサー、またはバックトラッキングLALR()パーサー(ただし、「拒否された」まま)パースを使用するか、Earleyパーサーを実行することです。GLRにはかなり優れたソリューションであるという利点があり、十分に文書化されており、今では十分に証明されています。GLLテクノロジーには、GLRを表示するためのかなり重要な利点がいくつかあります。
Ira Baxter

Rascalプロジェクト(オランダ)は、スキャナーなしのGLLパーサーを構築していると主張しています。作業中のため、オンライン情報を見つけるのが難しい場合があります。 en.wikipedia.org/wiki/RascalMPL
Ira Baxter

@IraBaxter GLLに新しい開発があるようです:GLL dotat.at/tmp/gll.pdf
Sjoerd

6

あなたは答えにかなり近いと思います。

LR(1)は、左から右への解析がコンテキストの先読みに1つのトークンのみを必要とすることを意味しますが、LR(∞)は、無限の先読みを意味します。つまり、パーサーは、それが現在どこにあるのかを理解するために、これから来るすべてを知る必要があります。


4
コンパイラクラスから、n> 0のLR(n)は数学的にLR(1)に還元できることを思い出します。n =無限大の場合はそうではありませんか?
rmeador 2008年

14
いいえ、nと無限大の違いの通過できない山があります。
ephemient

4
答えではありません:はい、無限の時間が与えられますか?:)
スティーブファローズ

7
実際、LR(n)-> LR(1)がどのように行われるかをぼんやりと思い出すと、新しい中間状態が作成されるため、ランタイムは「n」の非定数関数になります。LR(inf)-> LR(1)を変換すると、無限の時間がかかります。
Aaron、

5
「答えではありません:はい、無限の時間を与えられますか?」-いいえ:「無限の時間を与えられた」というフレーズは、「有限の時間を与えられて実行することはできない」という意味のない意味のない短い方法です。「無限」が表示されたら、「有限ではない」と考えてください。
ChrisW、2009年

4

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を作成したのは私です)。


これは、GCCが以前のLRパーサーとともに「x * y;」などのあいまいさを処理するために使用する標準的なハックです。悲しいかな、他の構造を解析するための任意の大きな先読み要件がまだあるので、LR(k)は固定kの解決策にはなりません。(GCCはより多くのアドホッカーで再帰的降下に切り替えました)。
Ira Baxter
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.