コンピュータサイエンスにおいて、演算子優先順位パーサーとは、演算子優先順位文法を解釈するボトムアップパーサーのことです。例えば、ほとんどの電卓は、演算子優先順位パーサーを使用して、演算順序に依存する人間が読みやすい中置記法から、逆ポーランド記法(RPN)などの評価に最適化された形式に変換します。
エドガー・ダイクストラのシャントヤードアルゴリズムは、演算子の優先順位を決定する構文解析器を実装する際によく用いられる。
演算子優先順位パーサーは、 LR(1)文法のサブセットを解析できる単純なシフトリデュースパーサーです。より正確には、演算子優先順位パーサーは、連続する2つの非終端記号とεがどの規則の右辺にも出現しないすべてのLR(1)文法を解析できます。
演算子優先順位パーサーは実際にはあまり使われませんが、より大規模な設計において役立つ特性をいくつか備えています。第一に、より高度な右シフト還元パーサーとは異なり、手書きで簡単に記述できます。第二に、実行時に演算子テーブルを参照するように記述できるため、解析中に演算子を追加または変更できる言語に適しています。(例としてHaskellがあり、Haskellではカスタムの結合規則と優先順位を持つユーザー定義の中置演算子が使用できます。そのため、参照されるすべてのモジュールの解析後に、プログラムに対して演算子優先順位パーサーを実行する必要があります。)
Raku は、速度とダイナミズムのバランスを取るために、 2 つの再帰下降パーサーの間に演算子優先順位パーサーを挟み込んでいます。GCC の C および C++ パーサーは、手書きの再帰下降パーサーですが、算術式を素早く調べることができる演算子優先順位パーサーによって両方とも高速化されています。演算子優先順位パーサーは、コンパイラが生成するパーサーにも組み込まれており、式解析の再帰下降アプローチを著しく高速化しています。[ 1 ]
優先順位上昇法は、マーティン・リチャーズとコリン・ウィットビー=ストレーベンスによって最初に記述された、式を解析するためのコンパクトで効率的かつ柔軟なアルゴリズムである。[ 2 ]
EBNF形式の中置記法による式文法は、通常次のようになります。
式:: =等式 等式:: =加算式( ( ' == ' | '! = ' )加算式) *加算式:: =乗算式( ( '+' | '-' )乗算式 ) *乗算式:: =主式( ( ' * ' | ' / ' )主式) *主式:: = ' ( '式' ) ' |数値|変数| '-'主式優先順位のレベルが多いため、予測型再帰下降パーサーでこの文法を実装すると非効率になる可能性があります。たとえば、数値を解析するには、5 つの関数呼び出しが必要になる場合があります。これは、プライマリに到達するまで文法内の各非終端記号に対して 1 回ずつ呼び出されるためです。
演算子優先順位パーサーは、同じことをより効率的に行うことができます。[ 1 ] この考え方は、同じ優先順位の演算子が見つかる限り算術演算を左結合できるが、優先順位の高い演算子を評価するために一時的な結果を保存する必要があるというものです。ここで紹介するアルゴリズムは明示的なスタックを必要とせず、代わりに再帰呼び出しを使用してスタックを実装します。
このアルゴリズムは、エドガー・ダイクストラのシャントヤードアルゴリズムのような純粋な演算子優先順位構文解析器ではありません。再帰下降構文解析器のように、主非終端記号が別のサブルーチンで解析されることを前提としています。
アルゴリズムの擬似コードは以下のとおりです。パーサーは関数parse_expressionから開始します。優先順位レベルは 0 以上です。
parse_expression() はparse_expression_1(parse_primary(), 0)を返します。
parse_expression_1(lhs, min_precedence) lookahead := peek next token while lookaheadは、優先順位が >= min_precedenceの二項演算子です。 op := lookahead 次のトークンに進む rhs := parse_primary () lookahead := peek next token while lookaheadは優先順位が高い二項演算子ですop ' s よりも、または右結合演算子優先順位がop のrhs と等しい場合:= parse_expression_1 ( rhs、opの優先順位+ (先読みの優先順位が大きい場合は 1、そうでない場合は 0)) lookahead := 次のトークンを覗き見る lhs :=オペランドlhsとrhsにopを適用した結果return lhsこのような生成規則の場合(演算子は一度しか出現できない)、次の点に注意してください。
等式:: =加算式( ' == ' | '! = ' )加算式アルゴリズムは、優先順位がmin_precedenceより大きい二項演算子のみを受け入れるように変更する必要があります。
式 2 + 3 * 4 + 5 == 19 の実行例は以下のとおりです。等式には 0、加算式には 1、乗算式には 2 の優先順位を与えます。
parse_expression_1 ( lhs = 2, min_precedence = 0)
1が返されます。
プラット構文解析として知られる別の優先順位構文解析器は、再帰的下降に基づいて、1973 年の論文「トップダウン演算子優先順位」[ 3 ]でヴォーン・プラットによって初めて記述されました。これは優先順位上昇よりも前に開発されたものですが、優先順位上昇の一般化と見なすことができます。[ 4 ]
プラットは当初、 CGOLプログラミング言語を実装するためにパーサーを設計し、彼の指導の下で修士論文でより詳細に扱われた。[ 5 ]
チュートリアルと実装例:
演算子の優先順位規則を適用する方法は他にもあります。その一つは、元の式のツリー構造を作成し、それにツリー書き換え規則を適用する方法です。
このようなツリーは、必ずしも従来ツリーに用いられてきたデータ構造を用いて実装する必要はありません。代わりに、トークンをテーブルなどのフラットな構造に格納し、同時にどの要素をどの順序で処理するかを示す優先順位リストを作成することができます。
別のアプローチとしては、まず式を完全に括弧で囲み、各演算子の周りに複数の括弧を挿入することで、線形左から右への構文解析でも正しい優先順位が得られるようにする方法があります。このアルゴリズムは初期のFORTRAN Iコンパイラで使用されていました。[ 7 ]
Fortran I コンパイラは、各演算子を括弧のシーケンスで展開します。アルゴリズムの簡略化された形式では、
+と をそれぞれ–と に置き換えます。))+(())-((*と をそれぞれ/と に置き換えます。)*()/(((各式の先頭と元の式の各左括弧の後にを追加します。))式の末尾と、元の式の各右括弧の前に追加します。一見分かりにくいかもしれないが、アルゴリズムは正しく、クヌースの言葉を借りれば、「信じられないかもしれないが、結果として得られる式は正しく括弧で囲まれている」[ 8 ]。
基本的な算術演算子( +、、、、、および)の括弧処理を行うシンプルなC言語アプリケーションの-サンプルコード:*/^()
#include <stdio.h> #include <string.h>// コマンドライン引数の境界が字句解析器です。int main ( int argc , char * argv []) { int i ; printf ( "((((" ); for ( i = 1 ; i != argc ; i ++ ) { // strlen(argv[i]) == 2 if ( argv [ i ] && ! argv [ i ][ 1 ]) { switch ( * argv [ i ]) { case '(' : printf ( "((((" ); continue ; case ')' : printf ( "))))" ); continue ; case '^' : printf ( ")^(" ); continue ; case '*' : printf ( "))*((" ); continue ; case '/' : printf ( " ))/( (" ); continue ; case '+' : // 単項チェック: 最初に演算子があるか、または二次引数を期待する演算子があるif ( i == 1 || strchr ( "(^*/+-" , * argv [ i -1 ])) printf ( "+" ); else printf ( ")))+(((" ); continue ; case '-' : if ( i == 1 || strchr ( "(^*/+-" , * argv [ i -1 ])) printf ( "-" ); else printf ( ")))-(((" ); continue ; } } printf ( "%s" ,argv [ i ]); } printf ( ")))) \n " ); return0 ; }まず、プログラムをコンパイルする必要があります。プログラムがC言語で記述されており、ソースコードがprogram.cという名前のファイルにあると仮定すると、次のコマンドを使用します。
gcc program.c -o program
上記のコマンドは、gcc に対して program.c をコンパイルし、program という名前の実行可能ファイルを作成するように指示します。
パラメータを指定してプログラムを実行するコマンド。例:a * b + c ^ d / e
./program a '*' b + c '^' d / e
それは生み出す
((((a))*((b)))+(((c)^(d))/((e))))
コンソールに出力されます。
この戦略の制約は、単項演算子はすべて中置演算子よりも高い優先順位を持つ必要があることです。上記のコードの「負」演算子はべき乗演算子よりも高い優先順位を持っています。この入力でプログラムを実行すると
- a^2
この出力を生成します
((((-a)^(2))))
それはおそらく意図されたものではないだろう。
この投稿の目的は、優先順位の上昇から始めて、コマンドパターンを使用するようにリファクタリングし、最終的にPrattパーサーに到達することです。[この著者は「優先順位の上昇」という用語を考案した人物です。]