私はこの文章を Dangling Else問題に関するWikipediaの記事 から理解できません。
[Dangling Elseの問題]は、コンパイラの構築、特にスキャナーを使用しない解析でよく発生する問題です。
誰かがスキャナーなしの解析技術がこの問題を悪化させるかもしれないことを私に説明できますか?問題は文法にあるようです-あいまいなので-構文解析手法の選択にあるのではありません。何が欠けていますか?
私の推測では、ウィキペディアの記事の文章は、E。ヴィッサーの作品に対する誤解から生じたものだと思います。
スキャナーレスパーサーの文法(つまり、トークンを文字列として個別に記述されたトークンのシーケンスのセットとしてではなく、文字のシーケンスのセットとして言語を記述する文法)は、あいまいさが多くなる傾向があります。 E. Visser論文 スキャナーなしの一般化LRパーサーの曖昧性除去フィルター (*)は、あいまいさを解決するためのいくつかのメカニズムを提案します。ぶら下がりのelseの問題を解決するのに役立ちます。しかし、この論文では、「ぶら下がりelse問題」と呼ばれる正確な曖昧さがスキャナーレスパーサーに関連しているとは述べていません(メカニズムがスキャナーレスパーサーに特に有用であることさえも)。
別のあいまいさの解決メカニズム(演算子の優先順位と優先順位)として、それがそれを解決するためのメカニズムを暗黙のステートメントではないことを提案しているという事実は、考慮されるパーサーのスキャナーなしの性質とはまったく無関係であるように見えます(たとえば、これらのあいまいさができないことを考慮してください)ネストの結果として通常の文法に存在しますが、最長一致ルールによって処理されるものは可能です)。
(*)これはおそらく、スキャナーなしのパーサーに関するウィキペディアの記事のベースとして機能している論文であり、別の論文を参照している場合でも、E。Visserによって Scannerless Generalized-LR Parsing 。
問題を述べるためだけに、Dangling Else Problemはコード構文仕様のあいまいさであり、次のifsとelsesの場合、どちらがifに属しているかがはっきりしない場合があります。
最も単純で古典的な例:
if(conditionA)
if(conditionB)
doFoo();
else
doBar();
if
がelse
を取得する言語仕様の詳細を知らない人にはわかりません(この特定のコードスニペットは6か国語以上で有効ですが、それぞれで異なる動作をする場合があります)。
Dangling Elseコンストラクトは、スキャナーなしのパーサー実装に潜在的な問題を引き起こします。なぜなら、パーサーがトークン化するのに十分であることをパーサーが確認するまで、ファイルストリームを一度に1文字ずつスローアップすることです(アセンブリまたはコンパイルする中間言語をダイジェストする)。 。これにより、パーサーは最小限の状態を維持できます。解析したトークンをファイルに書き込むのに十分な情報があると判断すると、すぐに書き込みを行います。これがスキャナーレスパーサーの最終目標です。高速、シンプル、軽量のコンパイル。
句読点の前後の改行と空白が(ほとんどのCスタイルの言語ではそうであるように)意味がないと仮定すると、このステートメントはコンパイラーに次のように表示されます。
if(conditionA)if(conditionB)doFoo();else doBar;
コンピューターで完全に解析できるので、見てみましょう。次の状態になるまで、一度に1文字ずつ取得します。
if(conditionA)
ああ、私はそれが(C#で)何を意味するかを知っています。これは、「Push
conditionAをevalスタックに入れ、次にbrfalse
を呼び出して、次のセミコロンの後にない場合、ステートメントにジャンプする」ことを意味します。現在、セミコロンは表示されないので、今のところ、この命令の後にジャンプオフセットを次のスペースに設定し、セミコロンが表示されるまで命令を挿入するたびに、そのオフセットを増分します。解析を続行しています...
if(conditionB)
OK、これは同様のIL操作のペアに解析され、解析したばかりの命令の直後に行きます。セミコロンが表示されないので、前のステートメントのジャンプオフセットを2つのコマンド(1つはPush用、もう1つはBreak用)の長さだけインクリメントして、調べ続けます。
doFoo();
それは簡単です。それが「call
doFoo」です。そして私が目にするのはセミコロンですか?まあ、それは素晴らしいことです、それは行の終わりです。これらの2つのコマンドの長さだけ、両方のブロックのジャンプオフセットをインクリメントします。では、次へ...
else
... ええとああ。これは見た目ほど単純ではありません。 OK、今何をしていたのか忘れてしまいましたが、else
は、すでに見たどこかに条件付きbreakステートメントがあることを意味します。振り返ってみましょう。そうです、brfalse
です。スタック上で、何であれ。では、次のステートメントとして無条件のbreak
が必要です。その後に続くステートメントは現在間違いなく私の条件付きブレークのターゲットなので、それが正しいことを確認し、入れた無条件のブレークを増分します。次に進みます...
doBar();
簡単だ。 「call
doBar」。セミコロンがあり、ブレースを見たことがない。したがって、無条件のbreak
は次のステートメントにジャンプする必要があります。それが何であれ、私はこれまで気にかけていたことを忘れることができます。
それで、私たちは何をしていますか...(注:10:00 PMであり、ビットオフセットを16進数に変換したり、関数の完全なILシェルにこれらを入力したりする気がしませんコマンドなので、これは通常バイトオフセットがある行番号を使用した疑似ILです):
ldarg.1 //conditionA
brfalse <line 6> //jumps to "break"
ldarg.2 //conditionB
brfalse <line 7> //jumps to "call doBar"
call doFoo
break <line 8> //jumps beyond statement in scope
call doBar
<line 8 is here>
まあ、それは実際に正しく実行されますが、(ほとんどのCスタイルの言語のように)ルールがelse
が最も近いif
を使用する場合です。実行のネストに従ってインデントされ、次のように実行されます。conditionAがfalseの場合、残りのスニペット全体がスキップされます。
if(conditionA)
if(conditionB)
doFoo();
else
doBar();
...しかし、それはセレンディピティによって行われます。これは、外側のif
ステートメントに関連付けられたブレークが、ステートメント全体を超えて実行ポインタをとるinnerbreak
の最後のif
ステートメントにジャンプするためです。これは余分な不要なジャンプであり、この例がさらに複雑な場合は、この方法で解析およびトークン化すると機能しなくなる可能性があります。
また、言語仕様でぶら下がっているelse
が最初のif
に属していると言っていて、conditionAがfalseの場合はdoBarが実行され、conditionAがtrueであるがconditionBでない場合は何も起こりません。
if(conditionA)
if(conditionB)
doFoo();
else
doBar();
パーサーは最初のif
が存在することを忘れていたため、この単純なパーサーアルゴリズムは、効率的なコードは言うまでもなく、正しいコードを生成しませんでした。
パーサーは、より長い間、if
sとelse
sを覚えておくのに十分賢いかもしれませんが、言語仕様が2つのelse
sが最初のif
と一致した後に単一のif
を示す場合、2つのif
sがelse
sと一致するという問題が発生します。
if(conditionA)
if(conditionB)
doFoo();
else
doBar();
else
doBaz();
パーサーは最初のelse
を確認し、最初のif
に一致し、次に2番目の方法を確認して、「地獄で何をしていたのか」モードでパニックになります。この時点で、パーサーは変更可能な状態でかなり多くのコードを取得しており、出力ファイルストリームにプッシュされているはずです。
これらすべての問題とwhat-ifの解決策があります。ただし、スマートにするために必要なコードにより、パーサーアルゴリズムの複雑さが増すか、またはパーサーをこのダムにできるようにする言語仕様により、end if
、またはif
ステートメントにelse
(両方とも他の言語スタイルでよく見られる)がある場合は、ネストされたブロックを示す括弧。
これは、いくつかのif
ステートメントの1つの単純な例にすぎません。コンパイラーが下す必要があるすべての決定と、とにかく簡単に混乱する可能性のある場所を調べます。これが、あなたの質問におけるウィキペディアの無害な声明の裏にある詳細です。