6 初歩的な電卓(3)

6.6 構文解析をしてから計算を実行させる

全体の流れ

scan() ->[トークンのリスト] -> parse() -> [構文解析木] -> calc()
自然言語
補足1 構文解析について

自然言語(日本語や英語など)でも、それを理解するために構文解析(parsing) は行われる

補足2 解析木の概要

数式を解析した木の場合、下図のような形のものができる。

構文解析の考え方1

解析1 (即時計算の時と同じ考え方)

以下を繰り返す
   トークンリストから、left op right の3つを取り出す
      取り出したものが left だけであれば繰り返しを終了
   3つをノードとして1つのデータ(ここでは配列)にまとめ、
    トークンリストの先頭に書き戻す

プログラムとしては以下のような形になる

while (left, op, right = tokenList.shift(3)) and op do
    tokenList.unshift [op, left, right]
end
p left  # ここに構文木が作られている
構文解析の考え方2

この形ではプログラムに発展性がないので、 トークンを1つずつ取り出して処理するプログラムにしてみる。 (ただし数式の文法違反の検出は当面は行わない)

トークンリストから予め1つ取り出して left に置いておく
トークンリストから1つずつトークンを取り出して以下の処理をする
   トークンが 演算子ならば op に置いておく
   トークンが 数値ならば、(そのトークンを最新トークンとすると)
        op left 最新トークン でノード(構文解析木のノード)を作り、
        left に置いておく
stack
構文解析の考え方3

スタック 先のプログラムでは変数を2つ(left, op) 使った (rightはすぐに消費するため使わなかった)が、 代わりに、スタック(LIFO型の1次元データ構造)を使う。

2. 優先度のある数式を扱う

優先度つき解析 (さらにしっかりしたプログラムにする)

優先度を考慮して構文解析を行うと、その結果の解析木は、 これまで見た例とは違い、(考え方1の右図に示した式 1+2*3 に対して) 右図の形になる。

以下のような手順で解析を行う。

トークンリストが空になるまで以下を繰り返す

とりだした先頭トークン tk が
  数字ならば、スタックにpush
  演算子ならば スタック[-2]にある演算子(op1と呼んでおく) を見て、  *
  1 op1が空(nil)ならば
    tk を スタックにpush
  2 tkの優先度 > op1の優先度 ならば
    tk を スタックにpush
  3 そうでなければ( tkの優先度 <= op1の優先度 )
    スタックから3つ組を取り出し  *
    ノードとしてまとめ、
    そのノードを再度スタックにpush した上で、
    最初に戻る
すべてのトークンを処理したら
    スタックから3つ組を取り出し、ノードとして再度push
    を繰り返す(3つ組が取り出せなくなるまで)

最後にスタックに1つだけ残った要素が、 構文木を表すタプル(多重のタプル)となる。

解析過程でのスタックの状態の変化(黒板図)を別ページに示す。 また、これを実現するプログラム例を以下に示す。

def parse1(tk,a)
  puts "#{tk} - #{a}"
  case tk
  when Numeric  then a.push tk      # 被演算子
  when Symbol then                  # 演算子
    if prior(tk) > prior(op1) then  # 優先度が高い => スタックに積む
      a.push tk
    else                            # 優先度が低い または同じ優先度
      l, op, r = a.pop(3)
      a.push [op, l, r]
      parse1(tk, a)                 # 再度呼ぶ(再帰)
    end
  end
end
def parse(tokenList)
  (tokenList+[:END]).each_with_object(a=[]){|tk,a|parse1(tk,a)}
  a[-2]
end
手順の要所
補足1 再帰について

頭山 再帰

補足2 プログラミングのテクニック
Hash
  1. op1が空の時の判断:
  2. 後処理(前述の「すべてのトークンを処理したら」)の実現:

3. 評価器

4. 演算子の優先度判断

注)

全体像

全体のデータの流れは右図の通り。

def scan(str)  # 数式を表す一行の文字列 str を受け取り
   ...
   # トークンの配列を返す
end

def parse(tokenList)  # トークンの配列を受け取り
   ...
   # 構文木を返す
end

def val(node)  # 構文木(を構成する1つのnode)を受け取り
   ...
   # そのnode(と下位node)を評価(計算)した値 を返す
end

### メインプログラム
while gets do
   puts val(parse(scan($_)))
end