15

これはしばらくの間私の心にありました。私は再帰降下パーサーに興味を持っており、その実装方法を知りたいと思っています。私が欲しいのは、「5+5」や「(5+5)*3」などの単純な算術演算を理解できる単純なパーサーです。

最初のステップは、入力文字列全体を取得して多くの部分文字列に分割する「トークナイザー」を作成することだと思います。私が行ったこの部分 (私はそれについてここで尋ねなければなりませんでした。ここにも関連コードを投稿しているので、したくない場合はリンクをたどる必要はありません。)私の、私は の 、またはトークンで終わりvectorますstring。ここで難しいのは、これらのトークンを解析したいということです。

ウィキペディアの recursive descent parsers に関する記事を読みました。全体的な概念は理解できますが、いつものように、実装は少しわかりにくいです。その記事には、非常に単純なプログラミング言語用の再帰降下パーサーの C 実装があり、記事でも説明されています。私はそのコードをできる限り研究し、基本的に同じことを書き込もうとしましたが、それは自分のプログラムのためでした。以下がそのコードです。

私が本当に混乱しているのは、このパーサーが何をするかです。プログラムを通過し、文法の特定の部分を「期待」しているようです。しかし、それがそこに到達すると、それは何をしますか? たとえば、「用語」を解析することになっているウィキペディア コードの 1 つの関数を次に示します。

void term(void) {
    factor();
    while (sym == times || sym == slash) {
        getsym();
        factor();
    }
}

これは、次の文法を解析するためのものです。

term = factor {("*"|"/") factor} .

これは理にかなっています。しかし、それは実際の用語で何をしますか? その用語が単に「6」である、または「3*2」であり、値が 6 であることが判明したとしましょう。それを残りの入力にどのように組み込むのでしょうか? (6 を返すために) の代わりにterm()a を返すべきではありませんか? それとも他の方法で行われますか?doublevoid

また、このようなパーサーでコードを出力することと、入力に対してすぐに動作すること (つまり、コンパイラーとインタープリター) の違いは何でしょうか? これら 2 つ (少なくともこの例では) は理論的には同じ方法で実装されていますか、それとも根本的に異なっていますか?

任意の入力を歓迎します。これまでの私のコードは次のとおりです。

#include <iostream>
#include <string>
#include <vector>
#include <ctype.h>
#include <sstream>

using namespace std;

vector<string> symbolize(string);
bool accept(string);
void getSymbol();
void error(string s);
bool expect(string);
void expression();
void term();
void factor();

int currentPosition = -1;
string symbol;
vector<string> symbols;

int main(int argc, const char * argv[])
{

    string input;
    getline(cin,input);

    symbols = symbolize(input);
    getSymbol();
    expression();


    return 0;
}

void factor(){
    if(isdigit(symbol.c_str()[0])){}
    else if(accept("(")){
        expression();
        expect(")");
    }
    else {
        error("Syntax error");
    }

}

void term(){
    factor();
    while(symbol=="*"||symbol=="/"){
        getSymbol();
        factor();
    }
}

void expression(){
    if(symbol == "+" || symbol == "-") getSymbol();
    term();
    while(symbol == "+" || symbol == "-"){
        getSymbol();
        term();
    }
}

void error(string s){
    cout << endl << "ERROR: " << s << endl;
}

void getSymbol(){
    currentPosition++;
    if(currentPosition>=symbols.size())error("Unexpectedly reached end of input");

}

bool expect(string s){
    if(accept(s))return true;
    else error("Expected '" + s + "'");
    return false;
}

bool accept(string s){
    if(s==symbol){getSymbol();return true;}
    return false;
}

// Takes a string and breaks it into substrings
vector<string> symbolize(string input){
    int position = 0;
    char c;
    //stringstream s;
    vector<string> symbols;
    enum symbolType {TEXT,OPERATOR}symbolType,charType;

    while(position < input.size()){
        stringstream s;
        c = input.at(position);
        if(isalnum(c))symbolType = TEXT;
        else symbolType = OPERATOR;
        charType = symbolType;

        while(symbolType == charType){
            s << c;
            position++;
            if(position>=input.length())break;
            c = input.at(position);
            if(isspace(c)||c=='\n'){position++; break;}
            if(isalnum(c)) charType = TEXT;
            else charType = OPERATOR;
        }

        symbols.push_back(s.str());
    }

    return symbols;
}

編集:私のコードは常にERROR: syntax error関数から: を出力することに言及する必要がありfactor()ます。

4

1 に答える 1

6

ウィキペディアの記事には、他に何もしない非常に完全に見えるパーサー (レクサーはありません!) が含まれています。

結果を実際に解釈するための一般的な考え方は、各解析関数が部分的に解釈された結果をその親/呼び出し元に返すことです。結果は、ツリー内のルールごとに異なるタイプになる可能性があります。完全な言語のパーサーを作成している場合、部分的に解釈された結果は単純な定数 (任意の型) になるか、関数全体 (後でコンパイルする必要がある場合があります) になる可能性があります。方程式パーサーの場合、各ルールは、他の関数の呼び出しから取得した要素に対して必要な操作を実行し、それを呼び出した関数に結果を返すだけです。

頭に浮かぶ2つのアプローチがあります。

  1. 各関数にsomething* resultパラメーターを受け入れさせます。単純な方程式パーサーの場合、これはおそらくfloat* resultすべての要素に当てはまります。

  2. void rule_x()...すべての関数を からに変更して、結果を返すだけfloat rule_x()...です。

どちらの場合でも、エラーを処理する何らかの方法が必要になります。C を使用している場合は例外がないため、おそらくオプション 1 を使用し、戻り値を使用して成功を示すのが最善でしょう。それならたくさんあるだろう

if(!expression(&result)) return 0;

しかし、C++ では、解析を例外ハンドラーでラップし、エラー時に例外をスローして、残りの解析を中止することができます。

たとえば、最適化または JIT を使用して言語全体をコンパイルし、構文エラーから適切に回復して解析を続けようとすると、事態はさらに興味深いものになります。

その題材となる本が龍書である。

于 2012-04-30T05:47:41.853 に答える