私は何年もプログラミングをしてきましたが、それでも非常に長い時間がかかる1つのタスクは、パーサーの文法を指定することです。そして、この過度の努力の後でさえ、私が思いついた文法が優れているかどうか確信が持てません( 「良い」の合理的な尺度による)。
文法を指定するプロセスを自動化するアルゴリズムがあるとは思いませんが、現在のアプローチの推測と試行錯誤の多くを排除する問題を構造化する方法があることを願っています。
私の最初の考えはパーサーについて読むことでした、そして私はこれのいくつかをしました、しかし私がこの主題で読んだすべては文法を与えられたものとして(または人が検査によってそれを指定できるほど十分に)、そして焦点を合わせますこの文法をパーサーに変換する問題。直前の問題、つまり最初に文法を指定する方法に興味があります。
私は主に、具体的な例(ポジティブとネガティブ)のコレクションを正式に表すgrammarを指定する問題に興味があります。これは、新しいsyntaxの設計の問題とは異なります。この違いを指摘してくれたMacneilに感謝します。
文法と構文の違いを本当に理解したことはありませんでしたが、それがわかり始めたので、主に文法を指定する問題に関心があると言って、最初の説明を明確にすることができました。事前定義された構文:たまたま私の場合、この構文の基礎は通常、正と負の例のコレクションです。
パーサーに文法はどのように指定されていますか?ベストプラクティス、設計方法論、およびパーサーの文法の指定に関するその他の有用な情報を説明するための事実上の標準である本または参考文献はありますか?パーサーの文法について読むとき、注意すべき点は何ですか?
サンプルファイルから、それらの例から一般化する量に基づいて決定を行う必要があります。次の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つ目は、これらのトークンタイプを使用する可能性のあるeverythingを受け入れるため、簡単です。 [このディスカッションでは、トークナイザーの設計についてあまり気にしていないと想定します。識別子、数字、句読点をトークンとして想定するのは簡単で、任意のスクリプト言語から任意のトークンセットを借りることができます。とにかく好きです。]
したがって、従う必要がある手順は、高レベルから開始して、「許可する各インスタンスの数」を決定することです。クラス内のmethod
sのように、構文構造が何回でも繰り返すことに意味がある場合は、次の形式のルールが必要になります。
methods ::= method methods | empty
これは [〜#〜] ebnf [〜#〜] でより適切に記述されています。
methods ::= {method}
ゼロまたは1つのインスタンスのみが必要な場合(つまり、Javaクラスのextends
句のように構成がオプションである場合)、または必要な場合1つまたは複数のインスタンスを許可するには(宣言の変数初期化子の場合と同様)、要素間に区切り文字が必要(引数リストの,
のように)、ターミネーターが必要などの問題に注意する必要があります。各要素の後(ステートメントを区切るための;
の場合と同様)、または(クラス内のメソッドの場合のように)セパレーターやターミネーターを必要としません。
言語で算術式を使用している場合、既存の言語の優先規則からコピーするのは簡単です。エキゾチックなものを探すよりも、Cの式のルールなどのよく知られているものを使用するのが最善ですが、他のすべてが等しい場合に限ります。
優先順位の問題(互いに解析されるもの)と繰り返しの問題(各要素の数が発生する必要がある、それらはどのように分離されるか)に加えて、順序についても考慮する必要があります。 1つのものが含まれている場合、別のものを除外する必要がありますか?
この時点で、いくつかのルールを文法的に適用したくなるかもしれません。たとえば、Person
の年齢が指定されている場合、それらの生年月日の指定も許可したくないというルールです。そうするために文法を構築することができますが、すべてが解析された後、「セマンティックチェック」パスを使用してこれを強制する方が簡単な場合があります。これは文法をシンプルに保ち、私の意見では、ルールに違反した場合のエラーメッセージを改善します。
パーサーの文法を指定する方法はどこで学べますか?
ほとんどのパーサージェネレーターでは、通常は Backus-Naur の一部のバリアントです<nonterminal> ::= expression
フォーマット。私は、あなたがそのようなものを使用していて、手でパーサーを構築しようとしているのではないという前提でいきます。構文が指定されている形式のパーサーを作成できる場合(以下にサンプル問題を含めました)、文法の指定は問題ではありません。
あなたが反対していると私が思うのは、サンプルのセットから構文を導き出すことです。これは、構文解析よりもパターン認識のほうが優れています。あなたがそれに頼らなければならないなら、それは彼らがそのフォーマットをうまく扱えないので、あなたのデータを提供している誰もがあなたにその構文を与えることができないことを意味します。反論して正式な定義を与えるように指示するオプションがある場合は、それを行います。悪い入力を受け入れる、または良い入力を拒否する、推測された構文に基づくパーサーの結果に対して責任を負うことができる場合、それらが漠然とした問題を与えることは公平ではありません。
...私が思いついた文法が適切であるかどうかは、決して確信できません(「妥当」の妥当な尺度で)。
あなたの状況で「良い」とは、「ポジティブを解析し、ネガティブを拒否する」ことを意味する必要があります。入力ファイルの構文の他の正式な仕様がない場合、サンプルは唯一のテストケースであり、それ以上のことはできません。あなたは足を下に置いて、良い例だけが良いものであり、他のものを拒絶すると言うことができますが、それはおそらくあなたが達成しようとしていることの精神ではありません。
正真正銘の状況では、文法のテストは他のもののテストに似ています。非ターミナルのすべてのバリアント(およびレクサーによって生成されている場合はターミナル)を実行するために十分なテストケースを考え出す必要があります。
サンプル問題
以下のルールで定義されているlistを含むテキストファイルを解析する文法を記述します。
入力例(すべて有効):
clank { foo = bar; baz = bear; }
clunk {
quux =bletch;
281_Apple = OU812;
He_Eats=Asparagus ; }
MacneilとBlrflの答えは素晴らしいです。プロセスの開始に関するコメントを追加したいだけです。
構文はプログラムを表すための単なる方法です。したがって、あなたの言語の構文は、この質問に対するあなたの答えによって決定されるべきです:プログラムとは何ですか?
プログラムはクラスの集合であると言うかもしれません。わかりました。
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つの目的を果たします。まず、抽象レベルでプログラムとは何かを理解するのに役立ちます。次に、言語の構造を表すために使用すべき具体的な構文を決定するのに役立ちます。
構造を理解したら、戻って空白やコメントの許可、あいまいさの修正などの問題に対処する必要があります。これらは重要ですが、全体的なデザインの副次的なものであり、使用している解析技術。
最後に、あなたの言語についてすべてを文法で表現しようとしないでください。たとえば、特定の種類の到達不能コード(たとえば、Javaのようにreturn
の後のステートメント)を禁止したい場合があります。あなたはおそらくそれを文法に詰め込もうとするべきではありません、なぜならあなたは何か(おっと、return
が中括弧にある場合はどうなるか、またはif
ステートメントの両方のブランチが返す場合はどうなるか?)そうしないと、文法が複雑になりすぎて管理できなくなります。 context-sensitive制約です。別のパスとして書きます。状況依存制約のもう1つの非常に一般的な例は、型システムです。十分に努力すれば、文法の1 + "a"
のような式を拒否できますが、1 + x
は拒否できません(x
には文字列型があります)。したがって、文法で半熟な制限を回避するを使用し、それらを個別のパスとして正しく実行します。