nekomata / regex-math /pattern-compiler.lisp
SNAPKITTYWEST's picture
push from SNAPKITTYWEST/nekomata
3b70664 verified
Raw History Blame Contribute Delete
10.7 kB
(in-package #:nekod.regex)
;;; NFA and DFA compilation from regex algebra
;;; Pipeline: REGEX_AST -> NFA -> epsilon-closure -> subset construction -> DFA
;;; --- NFA representation ---
(defstruct nfa-state
(id 0 :type integer)
(transitions nil :type list)
(epsilon nil :type list)
(accepting nil :type boolean))
(defstruct nfa
(states (make-array 0 :adjustable t :fill-pointer 0))
(initial 0 :type integer)
(accepting 0 :type integer))
(defvar *nfa-state-counter* 0)
(defun fresh-nfa-state (&optional accepting)
(let ((s (make-nfa-state :id (incf *nfa-state-counter*)
:accepting accepting)))
s))
;;; --- Thompson's construction: regex -> NFA ---
(defun regex-to-nfa (re)
"Convert regex algebra tree to NFA via Thompson's construction."
(let ((*nfa-state-counter* 0))
(regex-to-nfa-inner re)))
(defun regex-to-nfa-inner (re)
(etypecase re
(re-empty
(let ((s (fresh-nfa-state nil))
(f (fresh-nfa-state t)))
(make-nfa :states (make-array 2 :initial-contents (list s f) :adjustable t :fill-pointer 2)
:initial (nfa-state-id s)
:accepting (nfa-state-id f))))
(re-epsilon
(let ((s (fresh-nfa-state nil))
(f (fresh-nfa-state t)))
(push (nfa-state-id f) (nfa-state-epsilon s))
(make-nfa :states (make-array 2 :initial-contents (list s f) :adjustable t :fill-pointer 2)
:initial (nfa-state-id s)
:accepting (nfa-state-id f))))
(re-literal
(let* ((chars (re-literal-value re))
(states (loop for i from 0 to (length chars)
collect (fresh-nfa-state (= i (length chars))))))
(loop for i from 0 below (length chars)
for from-state = (nth i states)
for to-state = (nth (1+ i) states)
do (push (cons (char chars i) (nfa-state-id to-state))
(nfa-state-transitions from-state)))
(make-nfa :states (make-array (length states) :initial-contents states
:adjustable t :fill-pointer (length states))
:initial (nfa-state-id (first states))
:accepting (nfa-state-id (car (last states))))))
(re-concat
(let ((left (regex-to-nfa-inner (re-concat-left re)))
(right (regex-to-nfa-inner (re-concat-right re))))
(let ((left-accept (find (nfa-accepting left) (nfa-states left)
:key #'nfa-state-id)))
(setf (nfa-state-accepting left-accept) nil)
(push (nfa-initial right) (nfa-state-epsilon left-accept)))
(let ((all-states (concatenate 'vector (nfa-states left) (nfa-states right))))
(make-nfa :states (make-array (length all-states) :initial-contents all-states
:adjustable t :fill-pointer (length all-states))
:initial (nfa-initial left)
:accepting (nfa-accepting right)))))
(re-union
(let ((left (regex-to-nfa-inner (re-union-left re)))
(right (regex-to-nfa-inner (re-union-right re)))
(start (fresh-nfa-state nil))
(final (fresh-nfa-state t)))
(push (nfa-initial left) (nfa-state-epsilon start))
(push (nfa-initial right) (nfa-state-epsilon start))
(let ((left-accept (find (nfa-accepting left) (nfa-states left)
:key #'nfa-state-id))
(right-accept (find (nfa-accepting right) (nfa-states right)
:key #'nfa-state-id)))
(setf (nfa-state-accepting left-accept) nil)
(setf (nfa-state-accepting right-accept) nil)
(push (nfa-state-id final) (nfa-state-epsilon left-accept))
(push (nfa-state-id final) (nfa-state-epsilon right-accept)))
(let ((all (concatenate 'vector
(vector start final)
(nfa-states left)
(nfa-states right))))
(make-nfa :states (make-array (length all) :initial-contents all
:adjustable t :fill-pointer (length all))
:initial (nfa-state-id start)
:accepting (nfa-state-id final)))))
(re-kleene
(let ((inner (regex-to-nfa-inner (re-kleene-inner re)))
(start (fresh-nfa-state nil))
(final (fresh-nfa-state t)))
(push (nfa-initial inner) (nfa-state-epsilon start))
(push (nfa-state-id final) (nfa-state-epsilon start))
(let ((inner-accept (find (nfa-accepting inner) (nfa-states inner)
:key #'nfa-state-id)))
(setf (nfa-state-accepting inner-accept) nil)
(push (nfa-initial inner) (nfa-state-epsilon inner-accept))
(push (nfa-state-id final) (nfa-state-epsilon inner-accept)))
(let ((all (concatenate 'vector
(vector start final)
(nfa-states inner))))
(make-nfa :states (make-array (length all) :initial-contents all
:adjustable t :fill-pointer (length all))
:initial (nfa-state-id start)
:accepting (nfa-state-id final)))))
(re-plus
(regex-to-nfa-inner (re-concat (re-plus-inner re)
(re-kleene (re-plus-inner re)))))
(re-optional
(regex-to-nfa-inner (re-union re (make-re-epsilon))))))
;;; --- Epsilon closure ---
(defun find-nfa-state (nfa id)
(find id (nfa-states nfa) :key #'nfa-state-id))
(defun epsilon-closure (nfa state-ids)
"Compute epsilon-closure of a set of NFA state IDs."
(let ((closure (make-hash-table))
(stack (copy-list state-ids)))
(dolist (s state-ids) (setf (gethash s closure) t))
(loop while stack
do (let* ((sid (pop stack))
(state (find-nfa-state nfa sid)))
(when state
(dolist (eps (nfa-state-epsilon state))
(unless (gethash eps closure)
(setf (gethash eps closure) t)
(push eps stack))))))
(let ((result nil))
(maphash (lambda (k v) (declare (ignore v)) (push k result)) closure)
(sort result #'<))))
(defun nfa-move (nfa state-ids char)
"Compute set of states reachable from state-ids on input char."
(let ((result nil))
(dolist (sid state-ids)
(let ((state (find-nfa-state nfa sid)))
(when state
(dolist (tr (nfa-state-transitions state))
(when (char= (car tr) char)
(pushnew (cdr tr) result))))))
result))
;;; --- Subset construction: NFA -> DFA ---
(defstruct (dfa-state (:constructor make-dfa-state-struct))
id
transitions
accepting)
(defstruct dfa
states
initial
accepting)
(defun make-dfa-state (id &optional accepting)
(make-dfa-state-struct :id id
:transitions (make-hash-table :test #'equal)
:accepting accepting))
(defun dfa-add-transition (dfa from-id char to-id)
(let ((from-state (aref (dfa-states dfa) from-id)))
(setf (gethash char (dfa-state-transitions from-state)) to-id)))
(defun dfa-step (dfa state-id input-char)
(let ((state (aref (dfa-states dfa) state-id)))
(gethash input-char (dfa-state-transitions state))))
(defun dfa-run (dfa input)
"Run DFA on input string. Returns T if accepted."
(let ((current-state (dfa-initial dfa)))
(loop for c across input
do (let ((next (dfa-step dfa current-state c)))
(if next
(setf current-state next)
(return-from dfa-run nil))))
(dfa-state-accepting (aref (dfa-states dfa) current-state))))
(defun nfa-to-dfa (nfa)
"Convert NFA to DFA via subset construction."
(let* ((initial-closure (epsilon-closure nfa (list (nfa-initial nfa))))
(dfa-states-list nil)
(state-map (make-hash-table :test #'equal))
(state-counter 0)
(worklist nil)
(alphabet (collect-alphabet nfa)))
(setf (gethash initial-closure state-map) state-counter)
(push (cons initial-closure state-counter) dfa-states-list)
(incf state-counter)
(push initial-closure worklist)
(let ((transitions nil))
(loop while worklist
do (let* ((current (pop worklist))
(current-id (gethash current state-map)))
(dolist (c alphabet)
(let* ((moved (nfa-move nfa current c))
(target (and moved (epsilon-closure nfa moved))))
(when target
(unless (gethash target state-map)
(setf (gethash target state-map) state-counter)
(push (cons target state-counter) dfa-states-list)
(incf state-counter)
(push target worklist))
(push (list current-id c (gethash target state-map))
transitions))))))
(let* ((n state-counter)
(states (make-array n)))
(loop for i from 0 below n
do (let* ((entry (find i dfa-states-list :key #'cdr))
(nfa-ids (car entry))
(accepting (some (lambda (sid)
(let ((s (find-nfa-state nfa sid)))
(and s (nfa-state-accepting s))))
nfa-ids)))
(setf (aref states i) (make-dfa-state i accepting))))
(let ((result (make-dfa :states states :initial 0 :accepting nil)))
(dolist (tr transitions)
(dfa-add-transition result (first tr) (second tr) (third tr)))
(setf (dfa-accepting result)
(loop for i from 0 below n
when (dfa-state-accepting (aref states i))
collect i))
result)))))
(defun collect-alphabet (nfa)
"Collect all characters used in NFA transitions."
(let ((chars nil))
(loop for state across (nfa-states nfa)
do (dolist (tr (nfa-state-transitions state))
(pushnew (car tr) chars)))
chars))
;;; --- Top-level compilation ---
(defun compile-regex-to-dfa (re)
"Compile regex algebra tree directly to DFA. No runtime regex needed."
(nfa-to-dfa (regex-to-nfa re)))