パーサーの文法を指定するにはどうすればよいですか?


12

私は長年プログラミングを行ってきましたが、依然として非常に時間がかかるタスクの1つは、パーサーの文法を指定することです。この過度の努力の後でも、私が思いついた文法が良いかどうかはわかりません( 「適切」の合理的な尺度によって)。

文法を指定するプロセスを自動化するアルゴリズムがあるとは思っていませんが、現在のアプローチの当て推量と試行錯誤の多くを排除する問題を構築する方法があることを願っています。

私の最初の考えはパーサーについて読むことであり、私はこれのいくつかを行いましたが、私はこの主題について読んだすべてが文法を与えられたものとして(または検査によってそれを指定できるほど些細な)、そして焦点を当てますこの文法をパーサーに変換する問題。私は直前の問題に興味があります:そもそも文法を指定する方法。

私は主に、具体的な例(ポジティブとネガティブ)のコレクションを正式に表す文法を指定する問題に興味があります。これは、新しい構文を設計する問題とは異なります。この区別を指摘してくれたマクニールに感謝します。

文法と構文の違いを本当に理解したことは一度もありませんでしたが、理解し始めた今、私は主に文法を指定する問題に興味があると言って最初の明確化を明確にすることができました事前に定義された構文:私の場合、この構文の基礎は通常、ポジティブな例とネガティブな例の集まりです。

パーサーの文法はどのように指定されていますか?ベストプラクティス、設計方法論、およびパーサーの文法の指定に関するその他の有用な情報を説明するための事実上の標準である本または参考資料はありますか?パーサーの文法について読むとき、どの点に焦点を合わせるべきですか?


1
実際の問題に焦点を当てるために、質問を少し編集しました。このサイトはまさに、文法やパーサーについて質問したり、専門的な回答を得ることができる場所です。見る価値のある外部リソースがある場合、それらはあなたに直接役立つ答えに自然に現れます。
アダムリア

8
@kjoそれは残念です。必要なのが参照のリストだけである場合、Stack Exchangeを最大限に活用しているわけではありません。メタ問題は、意図したとおりにサイトを使用していません。リストの質問は、Q&Aモデルにあまり適合しないため、Stack Exchangeではほとんど一般的に推奨されていません。アイテム、アイデア、意見ではなく、答えのある質問をするように意識をシフトすることを強くお勧めします。
アダムリア

3
@kjoこれは確かに質問ですが、Stack Exchangeで尋ねるのは正しい質問ではありません。SEは、参照のリストを作成するためにここにはいません。ここ参考になります。詳細な説明については、コメントでリンクしたメタ投稿をお読みください。
アダムリア

5
@kjo:落胆しないでください!アンナの編集はあなたの質問の核心と核心を保ち、あなたがあなたの質問を私たちがProgrammers.SEに期待するより多くの形にすることであなたを助けました。あなたが求める明確な参考文献はありませんが、答えを出すことができました。[OTOH、そのような参考文献を知っていたら、確かにそれを含めたはずです。]この特定のケースでは、あなたが求めるものに参考文献があるとは思わないので、私のような答えをもっと奨励したいです。他の人と話すことからの経験。
マクニール

4
@kjoアンナの編集にロールバックし、本の推奨事項に関するガイダンスに基づいて標準参照の特定の呼び出しを組み込むことを試みました。提供された回答には多くの良い情報があり、本を見つけることだけに関する質問は無駄です。これで、編集の戦争ですべて停止できるなら、それは素晴らしいことです。

回答:


12

サンプルファイルから、これらの例からどれだけ一般化するかに基づいて決定する必要があります。次の3つのサンプルがあるとします:(それぞれが個別のファイルです)

f() {}
f(a,b) {b+a}
int x = 5;

これらのサンプルを受け入れる2つの文法を簡単に指定できます。

些細な文法1

start ::= f() {} | f(a,b) {b+a} | int x = 5;

些細な文法2:

start ::= tokens
tokens ::= token tokens | <empty>
token ::= identifier | literal | { | } | ( | ) | , | + | = | ;

最初のサンプルは3つのサンプルのみを受け入れるため、簡単です。2番目のものは、これらのトークンタイプを使用できる可能性のあるすべてを受け入れるため、簡単です。[この議論では、トークナイザーの設計にあまり関心がないと仮定します。トークンとして識別子、数字、句読点を仮定するのは簡単で、スクリプト言語からトークンセットを借りることができます。とにかく好き。]

したがって、従う必要がある手順は、高レベルで開始し、「許可する各インスタンスの数」を決定することです。methodクラス内のsのように、構文構造が何回でも繰り返す意味がある場合、次の形式のルールが必要になります。

methods ::= method methods | empty

EBNFでは次のように記述されています:

methods ::= {method}

インスタンスがゼロまたは1つだけの場合(extendsJavaクラスの節のように構成がオプションであることを意味する)、または1つ以上のインスタンスを許可する場合(宣言の変数初期化子の場合など) )。(,引数リストの場合のように)要素間にセパレーターを要求する、(;ステートメントを分離するために)各要素の後にターミネーターを要求する、またはセパレーターやターミネーターを要求しない(ケースのように)クラス内のメソッドで)。

言語で算術式を使用している場合、既存の言語の優先ルールからコピーするのは簡単です。Cの式のルールのようなよく知られた何かに固執するのが最善です。エキゾチックなものに行くよりも、他のすべてが等しい場合のみです。

優先順位の問題(相互に解析されるもの)および繰り返しの問題(各要素がいくつ発生するか、どのように分離されるか)に加えて、順序についても考慮する必要があります。あるものが含まれている場合、別のものを除外する必要がありますか?

この時点で、いくつかのルールを文法的に施行したくなるかもしれません。たとえば、Person年齢が指定されている場合、生年月日も指定したくないというルールです。そうするために文法を構築することができますが、すべてを解析した後に「セマンティックチェック」パスを使用してこれを実施する方が簡単な場合があります。これにより、文法が簡単になり、私の意見では、ルールに違反した場合のエラーメッセージが改善されます。


1
エラーメッセージを改善するために+1。あなたの言語のほとんどのユーザーは、1000万人でも1000万人でも、専門家ではありません。解析理論は、この側面をあまりにも長い間無視してきました。
–MSalters

10

パーサーの文法を指定する方法はどこで学べますか?

ほとんどのパーサージェネレーターでは、通常、Backus-Naur<nonterminal> ::= expression形式の一部のバリアントです。私は、あなたがそのようなものを使用しており、パーサーを手作業で構築しようとしないことを前提としています。構文が与えられた形式のパーサーを作成できる場合(以下にサンプルの問題を含めました)、文法を指定することは問題ではありません。

あなたが直面していると思うのは、サンプルのセットから構文を引き出すことです。これは、構文解析よりも実際にパターン認識です。それに頼らなければならない場合、データを提供している人はだれでもその形式をうまく処理できないため、構文を提供できません。押し戻して正式な定義を提供するように伝えるオプションがある場合は、それを行います。悪い入力を受け入れる、または良い入力を拒否する推定構文に基づいたパーサーの結果に対して責任を負うことができる場合、あいまいな問題を与えることは彼らにとって公平ではありません。

...私が思いついた文法が良いかどうかはわかりません(「良い」という合理的な尺度によって)。

あなたの状況で「良い」とは、「ポジティブを分析し、ネガを拒否する」ことを意味しなければなりません。入力ファイルの構文の他の正式な仕様がなければ、サンプルが唯一のテストケースであり、それ以上のことはできません。足を下ろして、良い例だけが良いと言い、他のことはすべて拒否することができますが、それはおそらくあなたが達成しようとしていることの精神ではありません。

正常な状況では、文法のテストは他のテストと同様です。非ターミナル(およびレクサーによって生成される場合はターミナル)のすべてのバリアントを実行するのに十分なテストケースを考え出す必要があります。


サンプル問題

以下の規則で定義されているリストを含むテキストファイルを解析する文法を作成します。

  • リストはゼロ以上で構成されてのもの
  • 事はで構成された識別子、オープンブレース、アイテムリストと閉じ括弧。
  • _item_list_は、ゼロ個以上のアイテムで構成されます
  • アイテムのconstsis 識別子、等号、別の識別子とセミコロン。
  • 識別子は、文字AZ、AZ、0-9またはアンダースコアのうちの1つまたは複数のシーケンスです。
  • 空白は無視されます。

入力の例(すべて有効):

clank { foo = bar; baz = bear; }
clunk {
    quux =bletch;
    281_apple = OU812;
    He_Eats=Asparagus ; }

2
また、BNF自体ではなく、「Backus-Naurのバリアント」を使用してください。BNFは文法を表現できますが、リストなどの非常に一般的な概念が必要以上に複雑になります。EBNFなど、これらの問題を改善するさまざまな改善されたバージョンがあります。
メイソンウィーラー

7

マクニールとBllflによる答えは素晴らしいです。プロセスの開始に関するコメントを追加したいだけです。

構文は表現するだけの方法でプログラムを。したがって、あなたの言語の構文は、この質問に対する答えによって決定されるべきです。

プログラムはクラスのコレクションであると言うかもしれません。さて、それは私たちに与えます

program ::= class*

出発点として。または、あなたはそれを書く必要があるかもしれません

program ::= ε
         |  class program

さて、クラスとは何ですか?名前があります。オプションのスーパークラス仕様。そして、多くのコンストラクター、メソッド、およびフィールドの宣言。また、クラスを単一の(明確な)ユニットにグループ化する何らかの方法が必要classです。これには、使いやすさ(たとえば、予約語でタグ付けする)が含まれる必要があります。はい:

class ::= "class" identifier extends-clause? "{" class-member-decl * "}"

それはあなたが選択できる1つの表記法(「具体的な構文」)です。または、これを簡単に決定できます。

class ::= "(" "class" identifier extends-clause "(" class-member-decl* ")" ")"

または

class ::= "class" identifier "=" "CLASS" extends-clause? class-member-decl* "END"

特に例を持っている場合は、おそらく暗黙のうちにこの決定をすでに行っているでしょうが、ポイントを補強したいだけです。構文の構造は、それが表すプログラムの構造によって決まります。それが、マクニールの答えから「ささいな文法」を乗り越えさせるものです。ただし、プログラム例は依然として非常に重要です。これらには2つの目的があります。まず、プログラムが何であるかを抽象レベルで把握するのに役立ちます。次に、言語の構造を表すために使用する具体的な構文を決定するのに役立ちます。

構造を取得したら、戻って空白やコメントの許可、あいまいさの修正などの問題に対処する必要があります。これらは重要ですが、設計全体に二次的であり、使用している解析技術。

最後に、あなたの言語に関するすべてを文法で表現しようとしないでください。たとえば、特定の種類の到達不能コード(returnJavaのように、の後のステートメントなど)を禁止したい場合があります。おそらくそれを文法に詰め込もうとするべきではありません。なぜなら、物事を忘れる(フープ、return括弧内にある場合、またはifステートメントの両方の分岐が戻る場合)か、文法を複雑にしすぎるからです。管理する。これは状況依存の制約です。別のパスとして書き込みます。状況依存制約のもう1つの非常に一般的な例は、型システムです。1 + "a"十分な努力をすれば、文法のように式を拒否できますが、拒否できません1 + xx文字列型の場合)。そう文法の中途半端な制限を避け、別のパスとして正しく実行してください。

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