Common LispでLALR(1)のparserを作る4
前回:Common LispでLALR(1)のparserを作る3 - nptclのブログ
1. 続きです
Common LispでLR(1)の表を作りました!
あとは実行してみて、そのあとでLALR(1)もやってみます。
2. 構文解析の実行
実行するためには、新たにスタックが必要になります。
すでに構文解析用のスタック*stack*
がありますが、あれも使いますし別のやつがもう一つ必要です。
*transit*
という名前で作ってみます。
(defvar *transit* nil) (defun push-transit (x) (declare (type unsigned-byte x)) (push x *transit*)) (defun pop-transit () (unless *transit* (error "pop-transit error.")) (pop *transit*)) (defun top-transit () (unless *transit* (error "top-transit error.")) (car *transit*)) (defun init-transit () (setq *transit* nil) (push-transit 0))
もうほとんど*stack*
と同じですね。
何が違うのかというと、*stack*
の方はshift/reduceで使いましたが、
*transit*
の方は、状態の番号を格納して使います。
こちらのスタックは、init-transit
で0
をpush
しています。
つまり初期状態が0
だということです。
それでは実行するためのコードを作ります。
(defun execute-parse (x) (init-stack) (init-transit) (set-input x) (call-execute))
execute-parse
は、*stack*
と*transit*
を初期化した後、
set-input
で入力を設定します。
つまり変数の初期化です。
そのあとcall-execute
を実行します。
(defun call-execute () (output-stack-input) (let* ((index (top-transit)) (state (get-state index)) (ahead (top-input))) (multiple-value-bind (type next) (find-action-state ahead state) (ecase type (s (shift-execute next) (call-execute)) (r (reduce-execute next) (call-execute)) (a (accept-execute))))))
call-execute
は、まず最初にoutput-stack-input
を実行して
*stack*
と*input*
の内容を出力します。
別に出力いらないや、っていうなら削除してください。
そのあと、top-transit
とget-state
で現在の状態を取得。
top-input
で、入力をひとつ先読みします。
先読みしたahead
と状態を引数に、find-action-state
で遷移先の情報を取得します。
もしShiftなら、shift-execute
を実行して再帰呼出でループ。
もしReduceなら、reduce-execute
を実行して再帰呼出でループ。
もしAcceptなら、accept-execute
を実行してcall-execute
を終了。
find-action-state
から順番に見ていきましょう。
(defun find-action-state (v state) (dolist (x (state-action state)) (destructuring-bind (a b c) x (when (eql a v) (return (values b c))))))
これは、状態state
のaction
から、記号v
に対応するものを検索しています。
見つからなかったらnil
が返却されますが、
呼び出し元のcall-execute
のecase
に引っかかってエラーになります。
それでは、Shiftが実行されたときの関数を見ていきます。
(defun shift-execute (next) (shift-test) (push-transit next))
shift-test
関数は、一番最初に確認として作った関数です。
やっていることは、input
からpop
して、stack
にpush
するというもの。
そのあと、push-transit
でnext
をpush
しています。
*transit*
の先頭の値が現在の状態になるので、
状態が変化するということになります。
Reduceを見てみましょう。
(defun reduce-execute (rule) (let ((left (rule-left rule)) (right (rule-right rule))) (reduce-call left right) (dolist (ignore right) (pop-transit)) (goto-execute)))
こちらはShiftとは違い複雑です。
まず規則からleft
とright
を取得し、reduce-call
を呼び出します。
reduce-call
も本当に最初の方に作った関数です。
*stack*
から必要な分だけpop
して、そのあとpush
するというものです。
これはこれであっているのですが、同じ数だけ*transit*
もpop
する必要があります。
ということで、dolist
でpop
しています。
Reduceの難しいところは、この状態でgoto
の遷移表を見る必要があるのです。
処理はgoto-execute
に引き継がれます。
(defun find-goto-state (v state) (dolist (x (state-goto state)) (destructuring-bind (a b) x (when (eql a v) (return b))))) (defun goto-execute () (let* ((index (top-transit)) (state (get-state index)) (sym (top-stack))) (aif (find-goto-state sym state) (push-transit it) (error "find-goto-state error, ~S, ~S." index sym))))
goto-execute
は、まず*transit*
から現在の状態を取得します。
さらに、*stack*
から一番上にある非終端記号を取得します。
取得した状態と、非終端記号をもとに、goto
表から次の状態を取得してpush-transit
で遷移します。
find-goto-state
関数は、非終端記号v
と状態state
からgoto
表を検索するというものです。
これでReduceの動作は完了です。
最後のaccept
を確認します。
(defun accept-execute () (format t "ACCEPT~%"))
ただ完了報告をしているだけです。
この状態で、*stack*
にはたった一つの開始記号が格納されています。
構文ツリーなんかを作っていた場合は、
このタイミングで取り出して終了となります。
3. LR(1)の実行テスト
それでは早速やってみましょう。
(parse-terminal + * [ ] int) (parse-start expr) (parse-rule expr -> expr + term) (parse-rule expr -> term) (parse-rule term -> term * fact) (parse-rule term -> fact) (parse-rule fact -> [ expr ]) (parse-rule fact -> int) (make-table) (make-reduce) (output-table) (execute-parse '(int * [ int + int ]))
実行結果は下記の通り。
0 : INT:S:19 [:S:6 FACT:20 TERM:21 EXPR:1 1 : END:A:NIL +:S:2 2 : INT:S:19 [:S:6 FACT:20 TERM:3 3 : END:R:1 +:R:1 *:S:4 4 : INT:S:19 [:S:6 FACT:5 5 : END:R:3 +:R:3 *:R:3 6 : INT:S:17 [:S:12 FACT:16 TERM:15 EXPR:7 7 : ]:S:18 +:S:8 8 : INT:S:17 [:S:12 FACT:16 TERM:9 9 : ]:R:1 +:R:1 *:S:10 10 : INT:S:17 [:S:12 FACT:11 11 : ]:R:3 +:R:3 *:R:3 12 : INT:S:17 [:S:12 FACT:16 TERM:15 EXPR:13 13 : ]:S:14 +:S:8 14 : ]:R:5 +:R:5 *:R:5 15 : ]:R:2 +:R:2 *:S:10 16 : ]:R:4 +:R:4 *:R:4 17 : ]:R:6 +:R:6 *:R:6 18 : END:R:5 +:R:5 *:R:5 19 : END:R:6 +:R:6 *:R:6 20 : END:R:4 +:R:4 *:R:4 21 : END:R:2 +:R:2 *:S:4 NIL (INT * [ INT + INT ]) (INT) (* [ INT + INT ]) (FACT) (* [ INT + INT ]) (TERM) (* [ INT + INT ]) (TERM *) ([ INT + INT ]) (TERM * [) (INT + INT ]) (TERM * [ INT) (+ INT ]) (TERM * [ FACT) (+ INT ]) (TERM * [ TERM) (+ INT ]) (TERM * [ EXPR) (+ INT ]) (TERM * [ EXPR +) (INT ]) (TERM * [ EXPR + INT) (]) (TERM * [ EXPR + FACT) (]) (TERM * [ EXPR + TERM) (]) (TERM * [ EXPR) (]) (TERM * [ EXPR ]) NIL (TERM * FACT) NIL (TERM) NIL (EXPR) NIL ACCEPT
うまくいってる!
長かった!
4. LALR(1)の作成
さあどんどん行きましょう。
LALR(1)はどうやって作るのでしょうか。
もし同じコアの集合が複数あるなら、ひとつにまとめてしまうのです。
コアとは、left
, alpha
, beta
のことであり、
コアが同じということはahead
だけが異なっているものです。
例えばこんな感じ。
([A -> B . C, a] [B -> . D, a/b] [B -> . E, c]) と ([A -> B . C, b] [B -> . D, b] [B -> . E, d/e]) が同じ
どうやってひとつにするかというと、ahead
をunion
します。
つまり次のようになります。
([A -> B . C, a/b] [B -> . D, a/b] [B -> . E, c/d/e])
いろんな方法があると思いますが、
ここでは新しい状態を作成しようと思います。
そのあと、マージ元の2つに向いている番号を、すべてマージ先に変更します。
マージと番号更新が終わったら、マージ元の全ての状態を削除します。
全然簡単じゃないですね。
書いてあることは難しくないのですが、
実際に作るとなれば結構な作業量になります。
でもやってみます。
もう飽きてきたので一気に説明します。
(defun make-lalr () (dolist (x *state*) (when (update-lalr x) (return (make-lalr)))))
make-lalr
はLR(1)をLALR(1)に変換する関数です。
やっていることは、全ての状態をdolist
で取得し、
ひとつずつupdate-lalr
で更新していくのですが、
もし更新が行われた場合はmake-lalr
を再帰呼出して最初からやり直します。
なにも更新が行われなくなった時点で完了です。
(defun update-lalr (x) (multiple-value-bind (merge remove) (split-lalr x *state*) (when merge (setq *state* remove) (let ((x (merge-lalr merge))) (replace-lalr x merge remove) t))))
update-lalr
は、引数の状態x
だけを見て、状態遷移表を更新する関数です。
split-lalr
は、コアが同じもののリストmerge
と、
それを除外したリストremove
を返却します。
merge-lalr
は、コアが同じ状態をまとめて新規の状態を作ります。
replace-lalr
は、全ての状態をたどって行き、遷移表の状態番号を更新します。
順番に見ていきます。
(defun split-lalr (x list) (when (find-others-lalr x list) (let (merge remove) (dolist (y list) (if (equal-state-lalr x y) (push y merge) (push y remove))) (values merge remove))))
split-lalr
は、状態x
と同じコアの集合と、違うコアの集合を返却します。
まず最初に、find-others-lalr
で自分以外の同じコアがあるかどうかを判定します。
存在するのであれば、dolist
を用いてmerge
とremove
を返却します。
(defun find-others-lalr (x list) (dolist (y list) (and (not (eql x y)) (equal-state-lalr x y) (return t))))
find-others-lalr
は、自分以外の同じコアを持つ状態を取得します。
同じコアかどうかの判定は、equal-state-lalr
で行います。
(defun equal-grammar-lalr (x y) (and (equal (grammar-left x) (grammar-left y)) (equal (grammar-alpha x) (grammar-alpha y)) (equal (grammar-beta x) (grammar-beta y)))) (defun equal-state-lalr (x y) (let ((x (state-list x)) (y (state-list y))) (equalset x y :test #'equal-grammar-lalr)))
equal-state-lalr
は、二つの集合に対して、equalset
で判定を行います。
同じコアかどうかは、equal-grammar-lalr
関数にて行います。
これで、同じコアの集合merge
が取得できました。
次にマージする関数を見ていきます。
(defun merge-lalr (list) (aprog1 (reduce #'merge-state-lalr list) (setf (state-index it) *state-index*) (push it *state*) (incf *state-index*)))
マージ作業は難しく考えたくなかったので、
Common Lispのreduce
関数に任せることにしました。
構文解析のreduce
とは関係ないので注意。
merge-state-lalr
関数とreduce
により、
全てをマージした状態がit
に束縛されます。
返却された状態it
は*state*
に登録されていない一時的なものなので、
aprog1
のbody部でちゃんと設定します。
通し番号の設定を行い、*state*
にpush
しています。
(defun merge-state-lalr (x y) (let ((list (merge-list-lalr x y)) (action (merge-action-lalr x y)) (goto (merge-goto-lalr x y))) (make-state :list list :action action :goto goto)))
これは単に集合list
と、action
, goto
をマージして、
新規の状態インスタンスを作成しているだけです。
(defun merge-grammar-lalr (x y) (grammar-instance (grammar-left x) (grammar-alpha x) (grammar-beta x) (union (grammar-ahead x) (grammar-ahead y)))) (defun merge-list-lalr (x y) (let ((listx (state-list x)) (listy (state-list y)) list) (dolist (v listx) (aif (find v listy :test #'equal-grammar-lalr) (push (merge-grammar-lalr v it) list) (error "Invalid state, ~S, ~S." x y))) list))
merge-list-lalr
は集合と集合をマージします。
dolist
で片方の集合をひとつずつ扱い、
find
でマージするべきもうひとつの要素を探します。
merge-grammar-lalr
は、ahead
をunion
で結合したインスタンスを返却します。
(defun merge-action-lalr (x y) (let ((x (state-action x)) (y (state-action y))) (union x y :test 'equal))) (defun merge-goto-lalr (x y) (let ((x (state-goto x)) (y (state-goto y))) (union x y :test 'equal)))
merge-action-lalr
は、状態のaction
表をマージします。
merge-goto-lalr
は、状態のgoto
表をマージします。
どちらもunion
とequal
だけで何とかなります。
マージ作業は以上です。
削除も完了していますので、あとは番号を置き換えるだけです。
(defun replace-lalr (x merge list) (let (a b) (setq b (state-index x)) (dolist (m merge) (setq a (state-index m)) (dolist (inst list) (replace-action-lalr inst a b) (replace-goto-lalr inst a b)))))
replace-lalr
は、2つのdolist
があります。
まずは削除された集合merge
で繰り返しを行い、古い番号を取得します。
新しい番号は、引数x
から取得します。
古い番号をa
、新しい番号をb
に代入しています。
2つめのdolist
で、状態一つ一つに対してa -> b
の置換を行っています。
(defun replace-action-lalr (inst a b) (let ((list (state-action inst))) (dolist (x list) (and (eq (cadr x) 's) (eql (caddr x) a) (setf (caddr x) b))) (setq list (delete-duplicates list :test 'equal)) (setf (state-action inst) list))) (defun replace-goto-lalr (inst a b) (let ((list (state-goto inst))) (dolist (x list) (when (eql (cadr x) a) (setf (cadr x) b))) (setq list (delete-duplicates list :test 'equal)) (setf (state-goto inst) list)))
replace-action-lalr
は、action
テーブルを置き換えます。
replace-goto-lalr
は、goto
テーブルを置き換えます。
どちらも遷移先を見つけては、setf
で強引に置き換えています。
置換後は重複が出てきますので、delete-duplicates
でひとつにしています。
駆け足で説明してきましたが、これでLALR(1)の処理は完了です。
5. LALR(1)の実行テスト
テストの内容はLR(1)と同じにします。
(parse-terminal + * [ ] int) (parse-start expr) (parse-rule expr -> expr + term) (parse-rule expr -> term) (parse-rule term -> term * fact) (parse-rule term -> fact) (parse-rule fact -> [ expr ]) (parse-rule fact -> int) (make-table) (make-reduce) (make-lalr) (output-table) (execute-parse '(int * [ int + int ]))
実行結果は下記の通り。
0 : INT:S:26 [:S:31 FACT:24 TERM:22 EXPR:1 1 : END:A:NIL +:S:23 22 : ]:R:2 +:R:2 END:R:2 *:S:27 23 : INT:S:26 [:S:31 FACT:24 TERM:25 24 : ]:R:4 +:R:4 *:R:4 END:R:4 25 : ]:R:1 +:R:1 END:R:1 *:S:27 26 : ]:R:6 +:R:6 *:R:6 END:R:6 27 : INT:S:26 [:S:31 FACT:29 28 : ]:R:5 +:R:5 *:R:5 END:R:5 29 : ]:R:3 +:R:3 *:R:3 END:R:3 30 : ]:S:28 +:S:23 31 : INT:S:26 [:S:12 FACT:24 TERM:22 EXPR:30 NIL (INT * [ INT + INT ]) (INT) (* [ INT + INT ]) (FACT) (* [ INT + INT ]) (TERM) (* [ INT + INT ]) (TERM *) ([ INT + INT ]) (TERM * [) (INT + INT ]) (TERM * [ INT) (+ INT ]) (TERM * [ FACT) (+ INT ]) (TERM * [ TERM) (+ INT ]) (TERM * [ EXPR) (+ INT ]) (TERM * [ EXPR +) (INT ]) (TERM * [ EXPR + INT) (]) (TERM * [ EXPR + FACT) (]) (TERM * [ EXPR + TERM) (]) (TERM * [ EXPR) (]) (TERM * [ EXPR ]) NIL (TERM * FACT) NIL (TERM) NIL (EXPR) NIL ACCEPT
うまくいってます。
遷移表はだいぶ変わっているのが分かります。
しかも短くなっています。
実行結果は当然変わらずです。