任意の文脈自由文法の解析、ほとんどが短い断片


20

ユーザー定義のドメイン固有の言語を解析したい。これらの言語は通常、数学表記に近いものです(自然言語を解析していません)。ユーザーは次のようにBNF表記でDSLを定義します。

expr ::= LiteralInteger
       | ( expr )
       | expr + expr
       | expr * expr

入力のようなもの1 + ( 2 * 3 )は受け入れられなければならず、入力のようなもの1 +は不正確であるとして拒否され、入力のようなもの1 + 2 * 3は曖昧なものとして拒否されなければなりません。

ここでの中心的な困難は、あいまいな文法をユーザーフレンドリーな方法で対処することです。文法を曖昧でないように制限することは選択肢ではありません。それが言語のやり方です。つまり、曖昧さを避けるために必要でない場合は、括弧を省略するほうが好ましいという考え方です。式が曖昧でない限り、解析する必要があり、そうでない場合は拒否する必要があります。

私のパーサーは、文脈に依存しない文法、あいまいなものでも動作し、すべての明確な入力を受け入れなければなりません。受け入れられたすべての入力に解析ツリーが必要です。無効またはあいまいな入力の場合、理想的には適切なエラーメッセージが必要ですが、最初に取得できるものを取り上げます。

私は通常、比較的短い入力でパーサーを呼び出しますが、入力は時々長くなります。したがって、漸近的に高速なアルゴリズムは最良の選択ではないかもしれません。入力が20シンボル未満の約80%、20〜50シンボルの19%、まれに1%のより長い入力の分布に最適化したいと思います。無効な入力の速度は大きな問題ではありません。さらに、DSLが1000〜100000入力ごとに変更されることを期待しています。数分ではなく、文法の前処理に数秒費やすことができます。

典型的な入力サイズを考慮して、どの解析アルゴリズムを調査する必要がありますか?エラー報告は私の選択の要因である必要がありますか、それとも明確な入力の解析に集中し、エラーフィードバックを提供するために完全に別個の低速なパーサーを実行する必要がありますか?

(それが必要なプロジェクト(しばらく前)では、CYKを使用しました。これは実装するのにそれほど難しくなく、入力サイズに適切に機能しましたが、非常に良いエラーを生成しませんでした。)


特に良いエラー報告を達成するのは難しいようです。曖昧な文法の場合に受け入れられる入力につながる複数のローカル変更があるかもしれません。
ラファエル

以下に答えました。すでに好評を博している古い質問の修正に答えるのは少し厄介です。明らかに、私は同様の方法で答えるべきではありませんが、ユーザーはあたかも同じ質問に答えているかのように両方の答えを読みます。
-babou

ユーザーが書き込みを行う場合には、実際には、曖昧な入力のためのエラーメッセージを期待してくださいx+y+z
babou

@babou質問を変更せず、コメントで要求された説明のみを追加しました(現在は削除されています)。ここに示した小さな文法では、の結合性を指定していません。そのため+x+y+z実際には曖昧であるため、誤りです。
ジル「SO-悪であるのをやめる」

さて、括弧の間にある場合でも、追加された最後の文です。あなたは言っているようです:私はついにCYKでそれをやったが、それはいくつかの理由でもはや適切ではありません。そして、私は正確な理由が何であるのだろうか...あなたは、あなたの種類の問題とあなたが使用する解決策について最も経験のある人です。
babou

回答:


19

おそらく、ニーズに最適なアルゴリズムは、一般化されたLL解析、またはGLLです。これは非常に新しいアルゴリズムです(この論文は2010年に公開されました)。ある意味では、グラフ構造スタック(GSS)で拡張されたEarleyアルゴリズムであり、LL(1)先読みを使用しています。

アルゴリズムは、文法がLL(1)でない場合に拒否しないことを除いて、単純な古いLL(1)と非常に似ています。解析のすべてのポイントに有向グラフを使用します。つまり、以前に処理された解析状態に遭遇した場合、これらの2つの頂点を単純にマージします。これにより、LLとは異なり、左再帰文法にも適しています。内部の仕組みの詳細については、この論文を読んでください(ラベルスープには多少の忍耐が必要ですが、非常に読みやすい論文です)。

このアルゴリズムには、他の一般的な解析アルゴリズム(私が知っている)よりも、ニーズに関連した多くの明確な利点があります。まず、実装は非常に簡単です。Earleyのみが実装が簡単だと思います。第二に、パフォーマンスは非常に優れています。実際、LL(1)の文法ではLL(1)と同じくらい速くなります。第三に、解析の回復は非常に簡単であり、複数の解析が可能かどうかを確認することも同様に簡単です。

GLLの主な利点は、LL(1)に基づいているため、実装時、文法の設計時、入力の解析時の理解とデバッグが非常に簡単であることです。さらに、エラー処理も容易になります。可能な解析が取り残された場所と、それらがどのように継続したかを正確に把握できます。エラーのポイント、たとえば解析が取り残された最後の3ポイントで、可能な解析を簡単に与えることができます。代わりに、エラーから回復することを選択し、最も遠い取得した解析がその解析に対して「完了」として作業していたプロダクションをマークし、解析がその後続行できるかどうかを確認できます(誰かが括弧を忘れた場合など)。たとえば、最も遠くにある5つの解析に対しても実行できます。

アルゴリズムの唯一の欠点は、それが新しいということです。つまり、すぐに利用できる確立された実装がないということです。これはあなたにとって問題ではないかもしれません-私はアルゴリズムを自分で実装しました、そしてそれはとても簡単でした。


新しいことを学ぶことができてうれしいです。私がこれを必要としたとき(数年前、いつか復活させたいプロジェクトで)、CYKを使用しました。GLLはあいまいな入力をどのように処理しますか?この記事ではこれについては説明していませんが、私はそれをざっと読みました。
ジル 'SO-悪であるのをやめる'

@Gilles:グラフ構造スタックを構築し、GLRの動作と同様に、すべての(潜在的に指数関数的に多くの)解析がこのグラフにコンパクトに表されます。私の記憶が正しければ、cstheory.stackexchange.com / questions / 7374 /で言及されている論文がこれを扱っています。
アレックス10ブリンク

@Gillesこの2010パーサーは、文法から手作業でプログラムする必要があるようです。複数の言語がある場合、または言語を頻繁に変更する場合は適切ではありません。選択された戦略(LL、LR、またはその他)に従って、一般的なパーサーの文法から自動生成し、すべての解析のフォレストを生成する手法は、約40年間知られています。ただし、解析を表すグラフの複雑さと編成に関する隠れた問題があります。解析の数は、指数よりも悪い場合があります:無限。エラー回復では、より体系的でパーサーに依存しない手法を使用できます。
-babou

GLLは、ANTLRで見つかったLL(*)とどのように関係しますか?
ラファエル

6

私の会社(Semantic Designs)はGLRパーサーを非常にうまく使用して、OPがドメイン固有の言語と「クラシック」プログラミング言語の両方の解析でDMS Software Reengineering Toolkitで解析することを提案しました。これは、大規模なプログラムの再構築/リバースエンジニアリング/フォワードコード生成に使用されるソースからソースへのプログラム変換をサポートします。これには、かなり実用的な方法での構文エラーの自動修復が含まれます。GLRを基盤として使用し、他のいくつかの変更(セマンティック述語、単なるトークン入力ではなくトークンセット入力など)を使用して、約40言語のパーサーを構築することができました。

GLRは、完全な言語インスタンスを解析する能力と同様に、ソースからソースへの書き換えルールの解析にも非常に役立つことが実証されています。これらは、完全なプログラムよりもコンテキストがはるかに少ないプログラムフラグメントであるため、一般にあいまいさが多くなります。ルールの解析中/解析後のあいまいさを解決するために、特別な注釈(たとえば、フレーズが特定の非終端記号に対応することを主張する)を使用します。GLR構文解析機構とその周辺のツールを整理することにより、言語のパーサーを取得したら、「無料」で書き換えルールのパーサーを取得します。DMSエンジンには組み込みの書き換えルールアプライヤーがあり、これを使用してこれらのルールを適用し、必要なコード変更を実行できます。

おそらく最も素晴らしい結果は、すべての曖昧さにもかかわらず、コンテキストなしの文法をベースとして使用して、完全なC ++ 14解析する機能です。私は、すべての古典的なC ++コンパイラ(GCC、Clang)がこれを行う能力を放棄し、手書きパーサーを使用していることに注意します(IMHOにより保守がはるかに困難になりますが、それは私の問題ではありません)。この機構を使用して、大規模なC ++システムのアーキテクチャを大幅に変更しました。

パフォーマンスに関しては、GLRパーサーはかなり高速です:毎秒数万行です。これは技術水準を大きく下回っていますが、これを最適化するための真剣な試みはしておらず、ボトルネックの一部は文字ストリーム処理(完全なUnicode)にあります。このようなパーサーを構築するには、LR(1)パーサージェネレーターに非常に近いものを使用して、コンテキストのない文法を前処理します。これは通常、C ++のサイズの大きな文法で10秒以内に最新のワークステーションで実行されます。驚くべきことに、最新のCOBOLやC ++のような非常に複雑な言語の場合、レクサーの生成には約1分かかります。Unicodeを介して定義されたDFAの一部はかなり毛深いものになります。私はちょうど指の運動としてRubyを(その信じられないほどの正規表現のための完全なサブ文法で)しました。DMSは、約8秒でレクサーと文法を一緒に処理できます。


@Raphael:「大規模な変更」リンクは、C ++アーキテクチャのリエンジニアリングに関するいくつか、DMSエンジン自体に関するもの(むしろ古いが基本はよく説明されている)、およびDMSの当初の動機付けであったデザインのキャプチャと再利用のエキゾチックなトピック(残念ながらまだ達成されていませんが、とにかくDMSは非常に有用であることが判明しました)。
アイラバクスター

1

(あいまいな文法に従って)あいまいな文を解析できる多くの一般的なコンテキストフリーパーサーがあります。それらはさまざまな名前、特に動的プログラミングまたはチャートパーサーに分類されます。最もよく知られているもので、最も単純なものの次には、おそらくあなたが使用しているCYKパーサーです。複数の解析を処理する必要があり、あいまいさを処理しているかどうかを最後までわからないため、その一般性が必要です。

あなたが言うことから、私はCYKはそれほど悪い選択ではないと思います。おそらく、予測性(LLまたはLR)を追加することで得られるものはそれほど多くなく、実際には、区別するのではなくマージする必要がある計算を区別することでコストがかかる場合があります(特にLRの場合)。また、生成される解析フォレストのサイズに対応するコストがかかる場合があります(あいまいさのエラーで役割を果たしている可能性があります)。実際、より洗練されたアルゴリズムの妥当性を正式に比較する方法はわかりませんが、CYKが優れた計算共有を提供することは知っています。

今、私は曖昧でない入力のみを受け入れるべき曖昧な文法のための一般的なCFパーサーに関する多くの文献があるとは思わない。おそらく技術文書やプログラミング言語でさえ、他の手段(ADA式のあいまいさなど)で解決できる限り、構文のあいまいさは許容されるため、私は見たことを覚えていません。

私は実際にあなたが持っているものに固執するのではなく、なぜあなたのアルゴリズムを変更したいのだろうと思っています。それは、どんな変化があなたに最も役立つかを理解するのに役立つかもしれません。それは速度の問題ですか、解析の表現ですか、それともエラーの検出と回復ですか?

複数の解析を表す最良の方法は、共有フォレストを使用することです。共有フォレストは、入力のみを生成するコンテキストフリーの文法ですが、DSL文法とまったく同じ解析ツリーを使用します。これにより、理解と処理が非常に簡単になります。詳細については、言語サイトで行ったこの回答をご覧になることをお勧めします。解析フォレストを取得することに興味がないことは理解していますが、解析フォレストの適切な表現は、あいまいさの問題についてより良いメッセージを提供するのに役立ちます。また、場合によっては、あいまいさは重要ではないと判断することもできます(結合性)。

あなたはDSL文法の処理時間の制約について言及していますが、そのサイズについては何のヒントも与えていません(これはあなたがやった数字で答えることができるという意味ではありません)。

一部のエラー処理は、これらの一般的なCFアルゴリズムに簡単な方法で統合できます。しかし、私はあなたがどんな種類のエラー処理がより肯定的であると予想するかを理解する必要があります。いくつか例を挙げてください。

私は本当にあなたの動機と制約が何であるか理解していないので、私はもっと言うのは少し気分が悪いです。あなたの言うことに基づいて、私はCYKに固執します(そして、私は他のアルゴリズムとその特性のいくつかを知っています)。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.