ラベル Automaton の投稿を表示しています。 すべての投稿を表示
ラベル Automaton の投稿を表示しています。 すべての投稿を表示

2010年3月17日水曜日

regexp->list->nfa

正規表現を文字列で考えていて頭がいたくなってきたので、一旦リストにしてからNFAにするようにしてみる。

;;regexp->list
(defun regexp->list (reg)
(let* ((top nil)
(stack (list top)))
(loop :for ch across reg
:do
(case ch
((#\()
(push top stack)
(setf top nil))
((#\))
(let ((prev (pop stack)))
(push top prev)
(setf top prev)))
((#\|)
(setf top
(list top :or)))
((#\*)
(let ((tmp (list (pop top) :loop)))
(push tmp top)))
((#\.)
(push :all top))
(T
(push ch top)))
:finally (return-from regexp->list
(reverse-tree top)))))

(defun reverse-tree (tree)
(if (listp tree)
(reverse (mapcar #'reverse-tree tree))
tree))

(defun list->nfa (lst)
(reduce
#'merge-nfa-concat
(remove
nil
(mapcar
#'(lambda (obj)
(typecase obj
(character
(make-nfa-char obj))
(list
(case (car obj)
((:loop)
(make-nfa-loop
(list->nfa (cdr obj))))
((:or)
(merge-nfa-or
(list->nfa (list (second obj)))
(list->nfa (cddr obj))))
(T
(list->nfa obj))))
(symbol
(case obj
((:all)
(make-nfa-all))
(T (error "Unexpected symbol:~A" obj))))
(T (error "Unexpected type:~A" obj))))
lst))))
>(match (list->nfa (regexp->list "(hoge|fuga|piyo)*"))
"foobarhogefugapiyofizzbuzz")
"hogefugapiyo"
6
18
>(match (list->nfa (regexp->list "'.*(hoge|fuga|piyo).*'"))
"this is 'test hoge.'")
"'test hoge.'"
8
20

NFAの状態遷移図をcl-dotで描画する

正規表現からNFAへ変換処理をデバッグするときに、NFAの状態遷移図どのようになったかを知りたい。手書きするのも面倒くさくなってきたので、cl-dotで描画することにした。

NFAは前回の記事の形式であるとする。

cl-dotのノードや矢印に属性値を指定したい場合、元々のオブジェクトをattributedというクラスのオブジェクトでラップすれば良いようだ。

;;;nfaの遷移図を描画
(defparameter *table* nil)
(defparameter *f* nil)

(defun mklist (lst)
(if (listp lst) lst (list lst)))

(defmethod cl-dot:graph-object-node ((graph (eql 'nfa)) (key symbol))
(make-instance 'cl-dot:node
:attributes
(list :label (format nil "~A" key)
:shape :box
:fontname "Arial"
:style :filled
:fillcolor
(if (member key *f*)"#aaaaaa" "#ffffff")
:color :black)))

(defmethod cl-dot:graph-object-points-to ((graph (eql 'nfa)) (key symbol))
(let ((alist (gethash key *table*)))
(mapcan
#'(lambda (lst)
(mapcar
#'(lambda (next)
(make-instance 'cl-dot:attributed :object next
:attributes (list
:label (format nil "~A" (car lst))
:dir :forward)))
(cdr lst)))
alist)))

(defun run (nfa path &key (format :png))
(let ((*table* (nfa-table nfa))
(*f* (mklist (nfa-f nfa))))
(let ((graph (cl-dot:generate-graph-from-roots
'nfa
(list (nfa-start nfa)))))
(cl-dot:dot-graph graph path :format format))))

;;(run nfa
;; "/home/kurohuku/edit/lisp/graphic/nfa.png")

正規表現からNFAを作る

ドラゴンブックの字句解析の項目を参考に、正規表現を表す文字列からNFAを作る。

(defvar *label-count* 0)

;;遷移図で特殊な入力記号として用いるもの
;; :epsilon イプシロン遷移
;; :all 任意の1文字

(defun mklist (obj)
(if (listp obj) obj (list obj)))

(defmacro do-hash ((key val) table &body body)
`(maphash
#'(lambda (,key ,val)
,@body)
,table))

(defun make-new-state ()
(incf *label-count*)
(intern (format nil "STATE-~A" *label-count*) :keyword))

(defun get-states (input move-table)
(or (cdr (assoc input move-table))
(if (eq input :epsilon)
nil
(cdr (assoc :all move-table)))))

;;tableはハッシュテーブルで、状態をキーとして、
;;入力とそれに対する遷移後の状態をalistで保存する
(defstruct NFA
start ;初期状態
table ;遷移図
f) ;受理状態の集合

(defun make-nfa-char (ch)
(let ((i (make-new-state))
(f (make-new-state)))
(let ((table (make-hash-table)))
(setf
(gethash i table)
(acons ch (list f) nil))
(make-nfa
:start i
:table table
:f (list f)))))

;;任意の一文字を受理する
(defun make-nfa-all ()
(let ((i (make-new-state))
(f (list (make-new-state)))
(table (make-hash-table)))
(setf (gethash i table)
(acons :all f nil))
(make-nfa
:start i
:f f
:table table)))


;;正規表現abを、a,bを表すNFA1,NFA2を連結して作成
(defun merge-nfa-concat (nfa1 nfa2)
(let ((i (nfa-start nfa1))
(nfa1f (mklist (nfa-f nfa1)))
(nfa2i (nfa-start nfa2))
(f (mklist (nfa-f nfa2))))
(let ((table (make-hash-table)))
(do-hash (s val) (nfa-table nfa1)
(setf
(gethash s table)
val))
(do-hash (s val) (nfa-table nfa2)
(dolist (move val)
;;nfa2の初期状態とnfa1の終了状態をくっつける
(if (eq s nfa2i)
(dolist (f nfa1f)
(push
move
(gethash f table)))
(push
move
(gethash s table)))))
(make-nfa
:start i
:f f
:table table))))

;;a|bをあらわすNFAを合成する
(defun merge-nfa-or (nfa1 nfa2)
(let ((newi (make-new-state))
(newf (make-new-state))
(table (make-hash-table)))
;;遷移図をコピー
(do-hash (key val) (nfa-table nfa1)
(setf (gethash key table) val))
(do-hash (key val) (nfa-table nfa2)
(setf (gethash key table) val))
;;新しい初期状態(newi)からのイプシロン遷移
(push
(list :epsilon (nfa-start nfa1) (nfa-start nfa2))
(gethash newi table))
;;新しい終了状態(newf)へのイプシロン遷移を追加
(dolist (f (mklist (nfa-f nfa1)))
(let ((old (gethash f table)))
(push
`(:epsilon ,newf ,@(get-states :epsilon old))
(gethash f table))))
(dolist (f (mklist (nfa-f nfa2)))
(let ((old (gethash f table)))
(push
`(:epsilon ,newf ,@(get-states :epsilon old))
(gethash f table))))
(make-nfa
:start newi
:f (list newf)
:table table)))

(defun make-nfa-loop (nfa)
(let ((newi (make-new-state))
(newf (make-new-state))
(table (make-hash-table)))
;;nfaのテーブルをコピー
(do-hash (k v) (nfa-table nfa)
(setf (gethash k table) v))
;;newiからnewfへのイプシロン遷移
;;newiから(nfa-start nfa)へのイプシロン遷移
(setf
(gethash newi table)
`((:epsilon ,(nfa-start nfa) ,newf)))
;;(nfa-f nfa)から(nfa-start nfa),newfへのイプシロン遷移
(dolist (f (mklist (nfa-f nfa)))
(let ((old (gethash f table)))
(push
`(:epsilon ,newf ,(nfa-start nfa) ,@(get-states :epsilon old))
(gethash f table))))
(make-nfa
:start newi
:f (list newf)
:table table)))

(defun nfa-start-states (nfa)
(move-epsilon (nfa-table nfa)
(nfa-start nfa)))

(defun move (nfa states input)
(let ((states (mklist states))
(table (nfa-table nfa)))
(move-epsilon
table
(move-inner table states input))))

(defun move-inner (table states input)
(if (not (listp states))
(get-states input (gethash states table))
(let ((result nil))
(dolist (s states)
(dolist (next (get-states input (gethash s table)))
(pushnew next result)))
result)))

(defun move-epsilon (table states)
(let ((unchecked (if (listp states) states (list states)))
(checked nil))
(do ((s (pop unchecked) (pop unchecked)))
((null s) checked)
(push s checked)
(dolist (next (move-inner table s :epsilon))
(unless (or (member next checked)
(member next unchecked))
(push next unchecked))))))

(defun regexp->nfa (str &optional (start 0))
(let ((len (length str))
(result nil))
(do ((i start (1+ i)))
((>= i len) (values
(reduce #'merge-nfa-concat
(nreverse result))
i))
(case (char str i)
((#\()
(multiple-value-bind (nfa next)
(regexp->nfa str (1+ i))
(push nfa result)
(setf i next)))
((#\))
(return-from regexp->nfa
(values
(reduce #'merge-nfa-concat
(nreverse result))
i)))
((#\*)
(let ((prev (pop result)))
(push (make-nfa-loop prev) result)))
((#\|)
(multiple-value-bind (nfa next)
(regexp->nfa str (1+ i))
(let ((prev
(reduce #'merge-nfa-concat
(nreverse result))))
(setf result nil)
(push
(merge-nfa-or prev nfa)
result))
(setf i next)))
((#\.)
(push (make-nfa-all) result))
(T
(push (make-nfa-char (char str i)) result))))))

(defun match (nfa str)
(let ((is (nfa-start-states nfa))
(f (nfa-f nfa))
(path nil)
(strlen (length str)))
(do ((begin 0 (1+ begin)))
((>= begin strlen) nil)
(setf path nil)
(do ((i 0 (1+ i))
(crr is crr))
((>= (+ begin i) strlen) nil)
(setf crr
(move nfa crr (char str (+ begin i))))
(if crr
(push crr path)
(setf i strlen))) ;次のループで終了する
(loop
:for sts in path
:for rest = path then (cdr rest)
:when (intersection sts f)
:do
(let ((end (+ begin (length rest))))
(return-from match
(values (subseq str begin end)
begin end)))))))

;;テスト用
(defun grep-file (reg file &optional (num nil))
(with-open-file (s file :direction :input)
(let ((nfa (regexp->nfa reg)))
(loop
:for line = (read-line s nil nil)
:for n from 1
:while line
:when (match nfa line)
:do
(if num
(format t "~A:~A~%" n line)
(format t "~A~%" line))))))

いちおう.*|の3種類を特別扱いしてくれるはず。今回のメインなのに、読み取りがうまく出来ないなぁ。

使ってみる。

>(match (regexp->nfa "'.*((lisp|scheme)|c++).*'")
"I like 'common lisp'")
"'common lisp'"
7
20
>(match (regexp->nfa "'.*((lisp|scheme)|c++).*'")
"I like 'white space'")
NIL

2010年3月7日日曜日

CommonLispで非決定性有限オートマトン

DFAの次は当然NFA。

NFAは現在取りうる状態の集合から、入力により次の状態の集合へと遷移する操作を繰り返し、最終的な状態の集合に受理状態が含まれていれば受理、そうでなければ拒否となると考えれば良いと思われる。これは、すべての遷移を並行処理しているようなイメージだろう。

'次の状態'には、イプシロン遷移で辿れるすべての状態を含む。

CommonLispでリストを集合として扱う関数にunion(nunion),set-difference(nset-difference) ,intersection(nintersection)などがある。

果たして正しく動いているのかどうか。

;;; Nondeterministic Finit Automaton

;;イプシロン遷移を表す特殊な入力記号として利用する
(defparameter *epsilon* '@)

(defclass NFA ()
((terminals :accessor terminals :initarg :terminals)
(move-functions :accessor move-functions :initarg :move-functions)
(entry-state :accessor entry-state :initarg :entry-state)
(all-states :accessor all-states :initarg :all-states)
(current-states :accessor current-states :initarg :current-states)))

;;;すべての状態を抜き出す
;;;結局使わないけれど。
(defun get-all-states (entry terminals rules)
(let ((states (copy-list (cons entry terminals))))
(loop :for rule in rules
:do
(pushnew (car rule) states)
(dolist (mv-rule (cdr rule))
(dolist (s (cdr mv-rule))
(pushnew s states))))
states))

(defmacro define-nfa (name (entry (&rest terminals)) &body rules)
`(defclass ,name (NFA)
((terminals :reader terminals :initform ',terminals)
(move-functions :reader move-functinos :initform ',rules)
(entry-state :reader entry-state :initform ',entry)
(all-states :reader all-states :initform (get-all-states ',entry ',terminals ',rules))
(current-states :accessor current-states
:initform (adjoin ',entry (move-epsilon ',entry ',rules nil))))))

;;;nfaが受理状態かどうか
(defun accept? (nfa)
(let ((terminals (terminals nfa)))
(loop :for s in (current-states nfa)
:when (find s terminals)
:do (return-from accept? t))
nil))

;;この時点ではイプシロン遷移しない。現在の状態に移ったときに完了しているはず。
(defun move (nfa input)
(let ((states (current-states nfa))
(result nil))
(let ((acc nil))
(dolist (s states)
(unless (eq s *epsilon*)
(push (move-inner s input (move-functions nfa)) acc)))
(dolist (next-states acc)
(dolist (s next-states)
(pushnew s result))))
(setf (current-states nfa) result)))

(defun move-inner (state input rules)
(let ((rule (assoc state rules)))
(let ((r (assoc input (cdr rule))))
(when r
(let ((result (cdr r)))
(reduce #'union
(mapcar
#'(lambda (s)
(move-epsilon s rules result))
result)
:initial-value result))))))

;;イプシロン遷移
;;tracked-statesは今までに現れた'次の状態'の集合
;;ループしたときに終了させる手段として保持
(defun move-epsilon (state rules tracked-states)
(let ((rule (assoc state rules)))
(let ((r (assoc *epsilon* (cdr rule))))
(when r
(let ((result (cdr r)))
(let ((next-tracked-states (union result tracked-states)))
(reduce #'union
(mapcar
#'(lambda (s)
(if (find s tracked-states)
nil
(move-epsilon s rules next-tracked-states)))
result)
:initial-value result)))))))

(defmethod run ((obj nfa) symbols)
(dolist (s symbols)
(setf (current-states obj)
(move obj s)))
(accept? obj))

(defmethod run ((obj symbol) symbols)
(let ((nfa (make-instance obj)))
(run nfa symbols)))

;;; テスト
(define-nfa test-nfa (:0 (:q :r))
(:0
(@ :q :r))
(:q
(0 :q)
(@ :a))
(:a
(0 :q))
(:r
(1 :r)))

;;"a(a|b)*bb"を受理する
(define-nfa test-nfa-2 (:i (:f))
(:i
(#\a :1))
(:1
(@ :2 :4))
(:2
(#\a :3)
(#\b :3))
(:3
(@ :2 :4))
(:4
(#\b :5))
(:5
(#\b :f)))

;;(trace move)
;;(run 'test-nfa '(0 1))
;;(run 'test-nfa '(1 1))
;;(run 'test-nfa-2 (coerce "aababb" 'list))

綺麗に書けたと納得のいくソースコードが書けるようになるのはいつの日のことか。

2010年2月28日日曜日

CommonLispで決定性有限オートマトン(DFA)

オートマトンの書籍を読んでいるので、CommonLispでDFAを定義してみる。

とりあえず目標として、記号列が(c c)で終わるものを受理するようなDFAを目指す。

(defmacro define-dfa (name (&rest ends) &body defs)
`(let ((table ',defs))
(defmethod accept-p ((type (eql ',name)) sym)
(find sym ',ends))
(defmethod next-state ((type (eql ',name)) input crr)
(let ((al (cdr (assoc crr table))))
(and al
(second (assoc input al)))))
(defmethod run ((type (eql ',name)) syms begin)
(let ((last begin))
(loop
:for sym in syms
:for state = last
:do (setf last (next-state type sym state))
:unless last
:do (return-from run :reject))
(if (accept-p type last)
:accept :reject)))))

正直ただしいのかわからない。

以下、DFAの定義。

(define-dfa cc (:end)
(:0
(a :0)
(b :0)
(c :1))
(:1
(a :0)
(b :0)
(c :end))
(:end
(a :0)
(b :0)
(c :end)))

存在する記号はa,b,cの3種類。記号列はリスト形式で受け取る。現在の状態と入力の組に対して、次の状態が存在しなければそこで処理を終了する。リスト内の要素すべてを処理したら、最終的な状態が受理状態であるかを調べる。

以下、実行例。

>(run 'cc '(c) :0)
:REJECT
>(run 'cc '(c c) :0)
:ACCEPT
>(run 'cc '(a b c) :0)
:REJECT
>(run 'cc '(a b c c) :0)
:ACCEPT
>(run 'cc '(a b c c c c) :0)
:ACCEPT
>(run 'cc '(a b c c a) :0)
:REJECT

ぱっと見た感じではうまく動いているような気がする。気のせいかもしれないけれど。