10分で書けるお手軽パーザー(構文解析器)

理系コンテンツ

t f B! P L
過去、パーザーに凝ってたので、ICPCで出題されるパーザーの交付をきちんと解答するやり方を書いている。
 コツを知っていれば誰でも迅速かつ正しく作成することができる。
方法
まず、方法を考えている。

基本的には、
・再帰下降型/ LL(1)文法の構文解析
・副作用を回避する
効率は無視する
こんな風に実装する。

最初は筆記パーザーの原則だ。
間違ってもLR構文解析を実装することができるなんてありえない。
1日では終わらないと思う。
そして、演算子の優先順位解析も提起しないことをお勧めする。
式にのみ適用することができる上にLLよりも若干アルゴリズムが厳しい。

第二は、バグなしのコードを一発で書き込みするには、非常に重要なことではないかと思う。
まともなCプログラマと副作用がある・なしを意識していること自体が少ないのだが。
最後は、細かいところを惜しみながら考えてコードを書くのが遅くなるという程度の意味だ。
LL構文解析
LL構文解析は、BNFで表現された文法を、トップダウン的に分析する方法で、 手でパーザーを書く場合、これを再帰的に関数呼び出しで実装される。

BNFだけ書けば後はそれをコードにするだけだ。
LL文法を実装する際に厄介なのは左再帰をどのようになるかということで、 それはLL(1)に該当しない文法をどう処理するかということだ。
左再帰
実際にはBNFから左再帰はかなりの頻度で出る。
これ右側再帰型に取る必要があるが、 構文解析の教科書には、なんだか難しいことが書いてある。

次のような構文があったかのように
    A = A x | y
これが
    A = y A '
    A '= x A' | ε

このように右再帰に修正することだ。
また、一般的に議論を展開すると左再帰を右再帰 "形式”で書き換えられているが、これらの変換を手だることは面倒で、何よりも完成文法が完全に直感的ではない。
再帰下降型のパーザーの手によって記述するから、何も形式的に変換する必要がないこと、 個々の場合 に応じてくれて簡単な方法で書けば良い。
そして、その場合だが、実際にはほとんどの繰り返しもある。

A = A x | ε
で、単純に0回以上出現したり、
E = E + T | T
など左結合演算子を、記述、主な事例は、この両方のだ。
まず右再帰に変換してもいいが、 ここ再帰下降型パーザーCで作成しようとしているのだから、 BNFにとらわれる必要はあ らない。

中括弧で囲まれた部分を繰り返しと定義
A = {x}
E = T {+ T}
このような感じが良い。
繰り返しEBNFにあるが、そんなものの存在も考慮する必要はない。)

(1)LLに該当しない文法の場合
再帰的に作成する場合
一つの先読みでは、次の呼び出し関数を決定することができないとダメだ。
例えば、Cの文法を考えると、intという文字をインポートすると、 次のfoo;で来る場合は、変数の宣言であり、foo(){...} で来る場合は、関数の宣言だ。
すなわち、この段階では、次の文字を読み込んで呼び出される関数が決定されていないため、Cの文法は、LL(1)ではないということだ。

これは再帰下降型では解析ることはできない。
可能な解決方法では、先読みの文字を増加させる、バックトラックなどがある。
文字を増やす場合
たとえば、前の例であれば、int fooまでロードしても構文要素を決定することができないが、次の文字が(あれば、関数の宣言と見ることができる。
すなわち、決めることができないところについて

決定されるまでの記号を先読みするということだ。
しかし、この場合、入力を順次読みこみながら解析するなど、コードを記述することが困難になって(今回の書くパーザーはそんなものではない が...)
また、判定の部分が煩雑になる。
より強力な方法は、バックトラックだ。
これは候補を全部試みるとだけだが、 Cとコードを書くことがやや複雑で、速度にちょっと不安だ。

だけど、構文クラスでは、LL(∞)は、このBNFで記述することができる構文クラスに一致する(...と思う)。
色々書いてきたが、それはとにかく面倒だ。
しかし、自信を持ってしてください。
パーザー系でLL(1)に遅れはほとんどない。
おそらく再帰下降で書くのが普通だからだと思うのだが。

実装
一般的に、1文字ずつ入力をロードしながら分析することが主流のような感じだが、ここで、読み取りを完全に分析する前にする。
これにより、分析関数からの副作用を削除する。
副作用の多いコードより動作が把握しやすくなると思う。
これらの仮定に基づいて関数の形を考えている。

入力は文字列、出力は分析の結果、加えて、分析し残した文字列を返する。
pair <​​分析結果の型、char *> parse(char * p)
{
  ...
}

通常、これらの関数を書いて並べることになる。
返り値は
typedef pair <​​分析結果、char *> parsed;
などとしておくと(タイピング)簡単だ。


構文解析の頻出、四則演算を実装しようとする。
とりあえず、構文を定義する。
Expr = Expr + Term | Expr - Term | Term
Term = Term * Fact | Term / Fact | Fact
Fact =(Expr)| number

右再帰を含んでいるので、次のように作成する。
文法はかなり適当だが。
Expr = Term {(+ | - )Term}
Term = Fact {(* | /)Fact}
Fact =(Expr)| number

このコードに落とす。
typedef pair <​​int、const char *> parsed;
parsed expr(const char * p);
parsed term(const char * p);
parsed fact(const char * p);

parsed expr(const char * p)
{
  parsed r = term(p)、  while(* r.second == '+' || * r.second == ' - '){
    char op = * r.second;
    int tmp = r.first;
    r = term(r.second + 1);
    if(op == '+')r.first = tmp + r.first;
    else r.first = tmp-r.first;
  }
  return r;
}

parsed term(const char * p)
{
  parsed r = fact(p);
  while(* r.second == '*' || * r.second == '/'){
    char op = * r.second;
    int tmp = r.first;
    r = fact(r.second + 1);
    if(op == '*')r.first = tmp * r.first;
    else r.first = tmp / r.first;
  }
  return r;
}

parsed fact(const char * p)
{
  if(isdigit(* p)){
    int t = *(p ++) - '0';
    while(isdigit(* p))t = t * 10 + *(p ++) - '0';
    return parsed(t、p);
  }
  else if(* p == '('){
    parsed r = expr(p + 1);
    if(* r.second!= ')')exit(0); // invalid input
    return parsed(r.first、r.second + 1);
  }
  else
    exit(0); // invalid input
}

いいから早く書いていことを目的にしているので、 字句解析と構文解析木の作成などは行わず、すべてパーズ中に実施している。
 これに、入出力部を付ければ一応完成だ。
 たとえば、1ラインごとに数式が含まれているような入力と
int main()
{
  string str;
  while(cin >> str)
    cout << expr(str.c_str())。 first << endl;
  return 0;
}

これでOKだ。
それと、上記のコード
char *にconst付け忘れ以外は間違いしていない。

エラー処理
空白のスキップなど
後は必要に応じて加え、 個々の問題について分析関数を適当に作成すれば大丈夫。
10分で作成できるか?

人気の投稿

このエントリーをはてなブックマークに追加

プロフィール

こんにちは!ゆうすけと申します。このブログでは、さまざまなジャンルやテーマについての情報やアイデアを共有しています。私自身、幅広い興味を持っており、食事、旅行、技術、エンターテイメント、ライフスタイルなど、幅広い分野についての情報を発信しています。日々の生活で気になることや、新しい発見、役立つヒントなど、あらゆる角度から情報を提供しています。読者の皆さんがインスパイアを受け、新しいアイデアを見つける手助けができれば嬉しいです。どのジャンルも一度に探求する楽しさを感じており、このブログを通じてその楽しさを共有できればと考えています。お楽しみに!

人気記事

ブログ アーカイブ

テキストの遊園地、vimの全オプション

このブログを検索

人気ブログランキングへ


QooQ