web-dev-qa-db-ja.com

再帰下降パーサーの実装

再帰下降パーサーの擬似コードを書きたいと思っています。今、私はこのタイプのコーディングの経験がありません。オンラインでいくつかの例を読みましたが、それらは数式を使用する文法でのみ機能します。これが私がパーサーの基礎としている文法です。

_S -> if E then S | if E then S else S | begin S L | print E

L -> end | ; S L

E -> i
_

メソッドS()L()、およびE()を記述し、いくつかのエラーメッセージを返す必要がありますが、オンラインで見つけたチュートリアルはあまり役に立ちませんでした。誰かが私を正しい方向に向けて、いくつかの例を教えてもらえますか?.

関連付けが簡単なので、C#またはJava構文で記述したいと思います。


更新

_public void S() {
    if (currentToken == "if") {
        getNextToken();
        E();

        if (currentToken == "then") {
            getNextToken();
            S();

            if (currentToken == "else") {
                getNextToken();
                S();
                Return;
            }
        } else {
            throw new IllegalTokenException("Procedure S() expected a 'then' token " + "but received: " + currentToken);
        } else if (currentToken == "begin") {
            getNextToken();
            S();
            L();
            return;
        } else if (currentToken == "print") {
            getNextToken();
            E();
            return;
        } else {
            throw new IllegalTokenException("Procedure S() expected an 'if' or 'then' or else or begin or print  token " + "but received: " + currentToken);
        }
    }
}


public void L() {
    if (currentToken == "end") {
        getNextToken();
        return;
    } else if (currentToken == ";") {
        getNextToken();
        S();
        L();
        return;
    } else {
        throw new IllegalTokenException("Procedure L() expected an 'end' or ';' token " + "but received: " + currentToken);
    }
}


public void E() {
    if (currentToken == "i") {
        getNextToken();
        return;
    } else {
        throw new IllegalTokenException("Procedure E() expected an 'i' token " + "but received: " + currentToken);
    }
}
_
17
user1072706

基本的に再帰下降構文解析では、文法の各非終端記号がプロシージャに変換され、各プロシージャ内で、現在表示しているトークンが非終端記号の右側に表示されるものと一致するかどうかを確認します。手順に対応する終端記号。それがプロダクションの適用を続行する場合、そうでない場合はエラーが発生したため、何らかのアクションを実行する必要があります。

したがって、上記のように、S()L()、およびE()の手順があります。ここで、L()の例を示します。実装すると、S()E()を自分で試してみることができます。

入力をトークン化するために他のプログラムが必要になることに注意することも重要です。そうすれば、そのプログラムに入力からの次のトークンを要求できます。

/**
 * This procedure corresponds to the L non-terminal
 * L -> 'end'
 * L -> ';' S L
 */ 
public void L()
{
   if(currentToken == 'end')
   {
      //we found an 'end' token, get the next token in the input stream
      //Notice, there are no other non-terminals or terminals after 
      //'end' in this production so all we do now is return
      //Note: that here we return to the procedure that called L()
      getNextToken();
      return; 
   } 
   else if(currentToken == ';')
   {
      //we found an ';', get the next token in the input stream
      getNextToken();
      //Notice that there are two more non-terminal symbols after ';'
      //in this production, S and L; all we have to do is call those
      //procedures in the order they appear in the production, then return
      S();
      L();
      return;
   }
   else
   {
      //The currentToken is not either an 'end' token or a ';' token 
      //This is an error, raise some exception
      throw new IllegalTokenException(
          "Procedure L() expected an 'end' or ';' token "+
          "but received: " + currentToken);
   }
}

ここで、S()E()を試して、投稿してください。

また、クリストファーが指摘しているように、あなたの文法にはぶら下がりと呼ばれるものがあります。つまり、同じことから始まり、ある時点までのプロダクションがあることを意味します。

S -> if E then S 
S -> if E then S else S

したがって、パーサーが「if」トークンを検出した場合、入力を処理するためにどのプロダクションを選択する必要があるのか​​という疑問が生じます。答えは、人間とは異なり、コンパイラは入力ストリームを先読みして「else」トークンを検索できないため、どちらを選択すればよいかわからないということです。これは、代数の問題を因数分解する方法と非常によく似た、左因数分解と呼ばれるルールを適用することで修正する簡単な問題です。

あなたがしなければならないのは、新しい非終端記号S '(S-prime)を作成することだけです。その右側には、一般的ではないプロダクションの断片が保持されるため、Sプロダクションは次のようになりません。

S  -> if E then S S'
S' -> else S 
S' -> e   
(e is used here to denote the empty string, which basically means there is no   
 input seen)
18
Hunter McMillen

最初のプロダクションルールには無制限の先読みがあるため、これは最初から始めるのが最も簡単な文法ではありません。

S -> if E then S | if E then S else S |  begin S L | print E

検討する

if 5 then begin begin begin begin ...

私たちはいつこの愚かな他の人を決定しますか?

また、考慮してください

if 5 then if 4 then if 3 then if 2 then print 2 else ...

さて、そのelseif 5 thenフラグメントにバインドすることになっていたのでしょうか?そうでない場合、それは実際にはクールですが、明示的にしてください。

文法を(おそらく、他のルールに応じて)次のように書き直すことができます。

S -> if E then S (else S)? | begin S L | print E
L -> end | ; S L
E -> i

どちらがあなたが望むものであるかもしれないし、そうでないかもしれません。しかし、疑似コードのようなものはこれから飛び出します。

define S() {
  if (peek()=="if") {
    consume("if")
    E()
    consume("then")
    S()
    if (peek()=="else") {
      consume("else")
      S()
    }
  } else if (peek()=="begin") {
    consume("begin")
    S()
    L()
  } else if (peek()=="print") {
    consume("print")
    E()
  } else {
    throw error()
  }
}

define L() {
  if (peek()=="end") {
    consume("end")
  } else if (peek==";")
    consume(";")
    S()
    L()
  } else {
    throw error()
  }
}

define E() {
  consume_token_i()
}

代替ごとに、一意のプレフィックスを調べるifステートメントを作成しました。マッチの試みの最後のelseは常にエラーです。キーワードを消費し、それらに遭遇すると、プロダクションルールに対応する関数を呼び出します。

擬似コードから実際のコードへの変換はそれほど複雑ではありませんが、簡単ではありません。それらのピークと消費は、おそらく実際には文字列を操作しません。トークンの操作ははるかに簡単です。そして、単に文を歩いてそれを消費することは、それを構文解析することとまったく同じではありません。ピースを消費するときに何かをしたいと思うでしょう。おそらく解析ツリーを構築します(つまり、これらの関数はおそらく何かを返します)。また、エラーをスローすることは大まかに正しいかもしれませんが、いくつかの意味のある情報をエラーに入れたいと思うでしょう。また、先読みが必要な場合は、事態はさらに複雑になります。

この種の問題を検討するときは、Terence Parr(再帰下降パーサージェネレーターであるantlrを作成した人)による言語実装パターンをお勧めします。 Dragon Book(Aho、et al、コメントで推奨)も良いです(それはおそらくコンパイラーコースの主要な教科書です)。

8
ccoakley

私は前学期にPLクラスの構文解析セクションを教えました(本当に助けました)。私たちのページから解析スライドを見るのを本当にお勧めします: ここ 。基本的に、再帰下降構文解析では、次の質問を自問します。

非終端記号を少し解析しましたが、次に何を解析するかを選択できるようになりました。次に見るものは、私がどの非終端記号にいるのかを決定します。

ちなみに、あなたの文法は、「ぶら下がり他」と呼ばれる非常に一般的なあいまいさを示しています。これは、ALGOLの時代から存在しています。シフトリデュースパーサー(通常はパーサージェネレーターによって生成されます)では、これによりシフト/リデュースの競合が発生します。通常、リデュースではなく任意にシフトすることを選択し、共通の「最大」プリンシパルを提供します。 (したがって、「if(b)then if(b2)S1 else S2」と表示された場合は、「if(b)then {if(b2){s1;} else {s2;}}」と読みます)

これを文法から外して、少し単純な文法で作業してみましょう。

T -> A + T
 |   A - T
 |   A
A -> NUM * A
   | NUM / A
   | NUM

また、NUMはレクサーから取得したものであると想定します(つまり、単なるトークンです)。この文法はLL(1)です。つまり、単純なアルゴリズムを使用して実装された再帰下降パーサーで解析できます。アルゴリズムは次のように機能します。

parse_T() {
  a = parse_A();
  if (next_token() == '+') {
    next_token();  // advance to the next token in the stream
    t = parse_T();
    return T_node_plus_constructor(a, t);
  }
  else if (next_token() == '-') {
    next_token();  // advance to the next token in the stream
    t = parse_T();
    return T_node_minus_constructor(a, t);
  } else {
    return T_node_a_constructor(a);
  }
}
parse_A() {
  num = token(); // gets the current token from the token stream
  next_token();  // advance to the next token in the stream
  assert(token_type(a) == NUM);
  if (next_token() == '*') {
    a = parse_A();
    return A_node_times_constructor(num, a);
  }
  else if (next_token() == '/') {
    a = parse_A();
    return A_node_div_constructor(num, a);
  } else {
    return A_node_num_constructor(num);
  }
}

ここにパターンがありますか:

文法の非終端記号ごとに:firstを解析してから、何を解析するかを決定するために何を見なければならないかを確認しますnext。完了するまでこれを続けてください!

また、このタイプの解析では通常、算術演算に必要なものが生成されないことに注意してください。再帰下降パーサー(末尾再帰で少しトリックを使用しない限り)は、左端の派生を生成しません。特に、左端の結合性が本当に必要な「a-> a--a」のような左再帰ルールを書くことはできません!これが、人々が通常、より洗練されたパーサジェネレータツールを使用する理由です。しかし、再帰下降トリックは、それでも知って遊んでみる価値があります。

3