123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129 |
- ;;;; fsm.lisp
- #|
- An FSM can be conveniently represented by means of a diagram. Until such a
- diagram is included as documentation, we'll have to content ourselves by its
- faithful representation by S-expressions, see e. g. the backgammon-machine
- definition in event.lisp
- We use a fairly straightforward implementation of an FSM as a CASE over the
- states, and a COND for each state choosing the arc. By default the event is
- ignored, i. e. we remain in the same state and do nothing.
- The choice of an arc is determined by the control state, the event, and the
- game state. However, the machine only needs the value of a few boolean
- variables to make the choice. Every COND-form choosing an arc is evaluated in
- a lexical scope where the variables are defined. This scope is introduced by
- the state-bindings function. As the set of variables and the rules for their
- computation varies from state to state (and from machine to machine, of
- course), state-bindings is a generic function dispatching on the names of the
- machine and the state. Perhaps a better dispatch can be thought of (when I
- used to have two machines, some code was duplicated), but anyway, it's a cool
- opportunity to use generic functions for code generation.
- The defmachine macro transform the description of a state machine into an event
- handler, whose arguments are the event and the controlled objects. Instead of
- inlining the actions into the handler, we look them up at run time in the table
- *machines*. This table assigns to each machine an alist of arcs. The keys of
- the alist are S-expressions representing the start state of the arc and its
- condition, the values are correspondent actions. Actually, this is a bit more
- cumbersome to implement than inlining and in theory, the runtime lookup is less
- efficient. However, the lookup is hardly the bottleneck. On the other hand,
- `interpreting' the machine allows to modify rules on the fly, which is
- convenient for small fixes, and we may be saving memory by not inlining the
- actions.
- Perhaps STATE-BINDINGS should be moved to run time as well?
- |#
- (in-package #:fsm)
- (eval-when (:compile-toplevel :load-toplevel :execute)
- (defgeneric state-bindings (machine state machine-arguments))
- ;; machine, state: symbols
- ;; condition: S-expression
- ;; args: the argumenst of the machine
- ;; A part of a COND form is generated: if condition holds, look up the action
- ;; associated with the symbolic form of the condition and apply it to args.
- (defun cond-clause (machine state condition args)
- `(,condition (funcall (second (assoc '(,state ,condition) (gethash ',machine *machines*) :test #'equal))
- ,@args)))
- ;; Collect the COND clauses into a COND form. In each branch, setf the
- ;; state-v variable to the value of the next state.
- ;; arcs: list of plists
- (defun state-cond (state-v machine state arcs args)
- `(cond ,@(let ((clauses (list `(t ,state-v))))
- (dolist (arc (reverse arcs) clauses)
- (push (append (cond-clause machine state (getf arc :when) args)
- (list `(setf ,state-v ',(getf arc :to))))
- clauses)))))
- ;; The CASE form over the states. The car of each rule is its type. We are
- ;; only interested in :state rules. For any such rule, (second rule) is its
- ;; name, and (cddr rule) is the list of arcs, each arc being represented by :arc
- ;; consed onto a plist.
- (defun states-case (state-v machine rules args)
- `(case ,state-v ,@(loop for rule in rules
- when (eql (first rule) :state)
- collect `(,(second rule) ,(funcall (state-bindings machine (second rule) args)
- (state-cond state-v
- machine
- (second rule)
- (loop for (head . tail) in (cddr rule)
- when (eql head :arc)
- collect tail)
- args))))))
- ;; When defining the handler, first register all the arcs.
- (defun arc-register-form (name arguments rules)
- `(progn
- ,@(loop for rule in rules
- when (eql (first rule) :state)
- append (loop for (head . tail) in (cddr rule)
- when (eql head :arc)
- collect `(register-arc ',name
- ',(second rule)
- ',(getf tail :when)
- (lambda (,@arguments)
- (declare (ignorable ,@arguments))
- ,(getf tail :action)))))))
- (defun handler-definition-form (name arguments rules state-v)
- `(defun ,name ()
- (let ((,state-v ',(second (assoc :initial-state rules))))
- (lambda (,@arguments)
- ,(states-case state-v name rules arguments)))))
- )
- (defvar *machines* (make-hash-table :test 'eq))
- (defun undefine-machine (machine)
- (remhash machine *machines*))
- (defun register-arc (machine state condition action)
- (push (list (list state condition)
- action)
- (gethash machine *machines*)))
- (defun call-arc (machine state condition &rest args)
- (let ((action (second (assoc (list state condition)
- (gethash machine *machines*)
- :test #'equal))))
- (when action
- (apply action args))))
- (defmacro defmachine (name (&rest arguments) &body rules)
- (let ((state-v (gensym "STATE-")))
- `(progn
- (undefine-machine ',name)
- ,(arc-register-form name arguments rules)
- ,(handler-definition-form name arguments rules state-v))))
|