1*061da546Spatrick#!/usr/bin/env python 2*061da546Spatrick 3*061da546Spatrick'''This module implements a Finite State Machine (FSM). In addition to state 4*061da546Spatrickthis FSM also maintains a user defined "memory". So this FSM can be used as a 5*061da546SpatrickPush-down Automata (PDA) since a PDA is a FSM + memory. 6*061da546Spatrick 7*061da546SpatrickThe following describes how the FSM works, but you will probably also need to 8*061da546Spatricksee the example function to understand how the FSM is used in practice. 9*061da546Spatrick 10*061da546SpatrickYou define an FSM by building tables of transitions. For a given input symbol 11*061da546Spatrickthe process() method uses these tables to decide what action to call and what 12*061da546Spatrickthe next state will be. The FSM has a table of transitions that associate: 13*061da546Spatrick 14*061da546Spatrick (input_symbol, current_state) --> (action, next_state) 15*061da546Spatrick 16*061da546SpatrickWhere "action" is a function you define. The symbols and states can be any 17*061da546Spatrickobjects. You use the add_transition() and add_transition_list() methods to add 18*061da546Spatrickto the transition table. The FSM also has a table of transitions that 19*061da546Spatrickassociate: 20*061da546Spatrick 21*061da546Spatrick (current_state) --> (action, next_state) 22*061da546Spatrick 23*061da546SpatrickYou use the add_transition_any() method to add to this transition table. The 24*061da546SpatrickFSM also has one default transition that is not associated with any specific 25*061da546Spatrickinput_symbol or state. You use the set_default_transition() method to set the 26*061da546Spatrickdefault transition. 27*061da546Spatrick 28*061da546SpatrickWhen an action function is called it is passed a reference to the FSM. The 29*061da546Spatrickaction function may then access attributes of the FSM such as input_symbol, 30*061da546Spatrickcurrent_state, or "memory". The "memory" attribute can be any object that you 31*061da546Spatrickwant to pass along to the action functions. It is not used by the FSM itself. 32*061da546SpatrickFor parsing you would typically pass a list to be used as a stack. 33*061da546Spatrick 34*061da546SpatrickThe processing sequence is as follows. The process() method is given an 35*061da546Spatrickinput_symbol to process. The FSM will search the table of transitions that 36*061da546Spatrickassociate: 37*061da546Spatrick 38*061da546Spatrick (input_symbol, current_state) --> (action, next_state) 39*061da546Spatrick 40*061da546SpatrickIf the pair (input_symbol, current_state) is found then process() will call the 41*061da546Spatrickassociated action function and then set the current state to the next_state. 42*061da546Spatrick 43*061da546SpatrickIf the FSM cannot find a match for (input_symbol, current_state) it will then 44*061da546Spatricksearch the table of transitions that associate: 45*061da546Spatrick 46*061da546Spatrick (current_state) --> (action, next_state) 47*061da546Spatrick 48*061da546SpatrickIf the current_state is found then the process() method will call the 49*061da546Spatrickassociated action function and then set the current state to the next_state. 50*061da546SpatrickNotice that this table lacks an input_symbol. It lets you define transitions 51*061da546Spatrickfor a current_state and ANY input_symbol. Hence, it is called the "any" table. 52*061da546SpatrickRemember, it is always checked after first searching the table for a specific 53*061da546Spatrick(input_symbol, current_state). 54*061da546Spatrick 55*061da546SpatrickFor the case where the FSM did not match either of the previous two cases the 56*061da546SpatrickFSM will try to use the default transition. If the default transition is 57*061da546Spatrickdefined then the process() method will call the associated action function and 58*061da546Spatrickthen set the current state to the next_state. This lets you define a default 59*061da546Spatricktransition as a catch-all case. You can think of it as an exception handler. 60*061da546SpatrickThere can be only one default transition. 61*061da546Spatrick 62*061da546SpatrickFinally, if none of the previous cases are defined for an input_symbol and 63*061da546Spatrickcurrent_state then the FSM will raise an exception. This may be desirable, but 64*061da546Spatrickyou can always prevent this just by defining a default transition. 65*061da546Spatrick 66*061da546SpatrickNoah Spurrier 20020822 67*061da546Spatrick 68*061da546SpatrickPEXPECT LICENSE 69*061da546Spatrick 70*061da546Spatrick This license is approved by the OSI and FSF as GPL-compatible. 71*061da546Spatrick http://opensource.org/licenses/isc-license.txt 72*061da546Spatrick 73*061da546Spatrick Copyright (c) 2012, Noah Spurrier <noah@noah.org> 74*061da546Spatrick PERMISSION TO USE, COPY, MODIFY, AND/OR DISTRIBUTE THIS SOFTWARE FOR ANY 75*061da546Spatrick PURPOSE WITH OR WITHOUT FEE IS HEREBY GRANTED, PROVIDED THAT THE ABOVE 76*061da546Spatrick COPYRIGHT NOTICE AND THIS PERMISSION NOTICE APPEAR IN ALL COPIES. 77*061da546Spatrick THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 78*061da546Spatrick WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 79*061da546Spatrick MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 80*061da546Spatrick ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 81*061da546Spatrick WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 82*061da546Spatrick ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 83*061da546Spatrick OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 84*061da546Spatrick 85*061da546Spatrick''' 86*061da546Spatrick 87*061da546Spatrickclass ExceptionFSM(Exception): 88*061da546Spatrick 89*061da546Spatrick '''This is the FSM Exception class.''' 90*061da546Spatrick 91*061da546Spatrick def __init__(self, value): 92*061da546Spatrick self.value = value 93*061da546Spatrick 94*061da546Spatrick def __str__(self): 95*061da546Spatrick return 'ExceptionFSM: ' + str(self.value) 96*061da546Spatrick 97*061da546Spatrickclass FSM: 98*061da546Spatrick 99*061da546Spatrick '''This is a Finite State Machine (FSM). 100*061da546Spatrick ''' 101*061da546Spatrick 102*061da546Spatrick def __init__(self, initial_state, memory=None): 103*061da546Spatrick 104*061da546Spatrick '''This creates the FSM. You set the initial state here. The "memory" 105*061da546Spatrick attribute is any object that you want to pass along to the action 106*061da546Spatrick functions. It is not used by the FSM. For parsing you would typically 107*061da546Spatrick pass a list to be used as a stack. ''' 108*061da546Spatrick 109*061da546Spatrick # Map (input_symbol, current_state) --> (action, next_state). 110*061da546Spatrick self.state_transitions = {} 111*061da546Spatrick # Map (current_state) --> (action, next_state). 112*061da546Spatrick self.state_transitions_any = {} 113*061da546Spatrick self.default_transition = None 114*061da546Spatrick 115*061da546Spatrick self.input_symbol = None 116*061da546Spatrick self.initial_state = initial_state 117*061da546Spatrick self.current_state = self.initial_state 118*061da546Spatrick self.next_state = None 119*061da546Spatrick self.action = None 120*061da546Spatrick self.memory = memory 121*061da546Spatrick 122*061da546Spatrick def reset (self): 123*061da546Spatrick 124*061da546Spatrick '''This sets the current_state to the initial_state and sets 125*061da546Spatrick input_symbol to None. The initial state was set by the constructor 126*061da546Spatrick __init__(). ''' 127*061da546Spatrick 128*061da546Spatrick self.current_state = self.initial_state 129*061da546Spatrick self.input_symbol = None 130*061da546Spatrick 131*061da546Spatrick def add_transition (self, input_symbol, state, action=None, next_state=None): 132*061da546Spatrick 133*061da546Spatrick '''This adds a transition that associates: 134*061da546Spatrick 135*061da546Spatrick (input_symbol, current_state) --> (action, next_state) 136*061da546Spatrick 137*061da546Spatrick The action may be set to None in which case the process() method will 138*061da546Spatrick ignore the action and only set the next_state. The next_state may be 139*061da546Spatrick set to None in which case the current state will be unchanged. 140*061da546Spatrick 141*061da546Spatrick You can also set transitions for a list of symbols by using 142*061da546Spatrick add_transition_list(). ''' 143*061da546Spatrick 144*061da546Spatrick if next_state is None: 145*061da546Spatrick next_state = state 146*061da546Spatrick self.state_transitions[(input_symbol, state)] = (action, next_state) 147*061da546Spatrick 148*061da546Spatrick def add_transition_list (self, list_input_symbols, state, action=None, next_state=None): 149*061da546Spatrick 150*061da546Spatrick '''This adds the same transition for a list of input symbols. 151*061da546Spatrick You can pass a list or a string. Note that it is handy to use 152*061da546Spatrick string.digits, string.whitespace, string.letters, etc. to add 153*061da546Spatrick transitions that match character classes. 154*061da546Spatrick 155*061da546Spatrick The action may be set to None in which case the process() method will 156*061da546Spatrick ignore the action and only set the next_state. The next_state may be 157*061da546Spatrick set to None in which case the current state will be unchanged. ''' 158*061da546Spatrick 159*061da546Spatrick if next_state is None: 160*061da546Spatrick next_state = state 161*061da546Spatrick for input_symbol in list_input_symbols: 162*061da546Spatrick self.add_transition (input_symbol, state, action, next_state) 163*061da546Spatrick 164*061da546Spatrick def add_transition_any (self, state, action=None, next_state=None): 165*061da546Spatrick 166*061da546Spatrick '''This adds a transition that associates: 167*061da546Spatrick 168*061da546Spatrick (current_state) --> (action, next_state) 169*061da546Spatrick 170*061da546Spatrick That is, any input symbol will match the current state. 171*061da546Spatrick The process() method checks the "any" state associations after it first 172*061da546Spatrick checks for an exact match of (input_symbol, current_state). 173*061da546Spatrick 174*061da546Spatrick The action may be set to None in which case the process() method will 175*061da546Spatrick ignore the action and only set the next_state. The next_state may be 176*061da546Spatrick set to None in which case the current state will be unchanged. ''' 177*061da546Spatrick 178*061da546Spatrick if next_state is None: 179*061da546Spatrick next_state = state 180*061da546Spatrick self.state_transitions_any [state] = (action, next_state) 181*061da546Spatrick 182*061da546Spatrick def set_default_transition (self, action, next_state): 183*061da546Spatrick 184*061da546Spatrick '''This sets the default transition. This defines an action and 185*061da546Spatrick next_state if the FSM cannot find the input symbol and the current 186*061da546Spatrick state in the transition list and if the FSM cannot find the 187*061da546Spatrick current_state in the transition_any list. This is useful as a final 188*061da546Spatrick fall-through state for catching errors and undefined states. 189*061da546Spatrick 190*061da546Spatrick The default transition can be removed by setting the attribute 191*061da546Spatrick default_transition to None. ''' 192*061da546Spatrick 193*061da546Spatrick self.default_transition = (action, next_state) 194*061da546Spatrick 195*061da546Spatrick def get_transition (self, input_symbol, state): 196*061da546Spatrick 197*061da546Spatrick '''This returns (action, next state) given an input_symbol and state. 198*061da546Spatrick This does not modify the FSM state, so calling this method has no side 199*061da546Spatrick effects. Normally you do not call this method directly. It is called by 200*061da546Spatrick process(). 201*061da546Spatrick 202*061da546Spatrick The sequence of steps to check for a defined transition goes from the 203*061da546Spatrick most specific to the least specific. 204*061da546Spatrick 205*061da546Spatrick 1. Check state_transitions[] that match exactly the tuple, 206*061da546Spatrick (input_symbol, state) 207*061da546Spatrick 208*061da546Spatrick 2. Check state_transitions_any[] that match (state) 209*061da546Spatrick In other words, match a specific state and ANY input_symbol. 210*061da546Spatrick 211*061da546Spatrick 3. Check if the default_transition is defined. 212*061da546Spatrick This catches any input_symbol and any state. 213*061da546Spatrick This is a handler for errors, undefined states, or defaults. 214*061da546Spatrick 215*061da546Spatrick 4. No transition was defined. If we get here then raise an exception. 216*061da546Spatrick ''' 217*061da546Spatrick 218*061da546Spatrick if (input_symbol, state) in self.state_transitions: 219*061da546Spatrick return self.state_transitions[(input_symbol, state)] 220*061da546Spatrick elif state in self.state_transitions_any: 221*061da546Spatrick return self.state_transitions_any[state] 222*061da546Spatrick elif self.default_transition is not None: 223*061da546Spatrick return self.default_transition 224*061da546Spatrick else: 225*061da546Spatrick raise ExceptionFSM ('Transition is undefined: (%s, %s).' % 226*061da546Spatrick (str(input_symbol), str(state)) ) 227*061da546Spatrick 228*061da546Spatrick def process (self, input_symbol): 229*061da546Spatrick 230*061da546Spatrick '''This is the main method that you call to process input. This may 231*061da546Spatrick cause the FSM to change state and call an action. This method calls 232*061da546Spatrick get_transition() to find the action and next_state associated with the 233*061da546Spatrick input_symbol and current_state. If the action is None then the action 234*061da546Spatrick is not called and only the current state is changed. This method 235*061da546Spatrick processes one complete input symbol. You can process a list of symbols 236*061da546Spatrick (or a string) by calling process_list(). ''' 237*061da546Spatrick 238*061da546Spatrick self.input_symbol = input_symbol 239*061da546Spatrick (self.action, self.next_state) = self.get_transition (self.input_symbol, self.current_state) 240*061da546Spatrick if self.action is not None: 241*061da546Spatrick self.action (self) 242*061da546Spatrick self.current_state = self.next_state 243*061da546Spatrick self.next_state = None 244*061da546Spatrick 245*061da546Spatrick def process_list (self, input_symbols): 246*061da546Spatrick 247*061da546Spatrick '''This takes a list and sends each element to process(). The list may 248*061da546Spatrick be a string or any iterable object. ''' 249*061da546Spatrick 250*061da546Spatrick for s in input_symbols: 251*061da546Spatrick self.process (s) 252*061da546Spatrick 253*061da546Spatrick############################################################################## 254*061da546Spatrick# The following is an example that demonstrates the use of the FSM class to 255*061da546Spatrick# process an RPN expression. Run this module from the command line. You will 256*061da546Spatrick# get a prompt > for input. Enter an RPN Expression. Numbers may be integers. 257*061da546Spatrick# Operators are * / + - Use the = sign to evaluate and print the expression. 258*061da546Spatrick# For example: 259*061da546Spatrick# 260*061da546Spatrick# 167 3 2 2 * * * 1 - = 261*061da546Spatrick# 262*061da546Spatrick# will print: 263*061da546Spatrick# 264*061da546Spatrick# 2003 265*061da546Spatrick############################################################################## 266*061da546Spatrick 267*061da546Spatrickimport sys 268*061da546Spatrickimport string 269*061da546Spatrick 270*061da546SpatrickPY3 = (sys.version_info[0] >= 3) 271*061da546Spatrick 272*061da546Spatrick# 273*061da546Spatrick# These define the actions. 274*061da546Spatrick# Note that "memory" is a list being used as a stack. 275*061da546Spatrick# 276*061da546Spatrick 277*061da546Spatrickdef BeginBuildNumber (fsm): 278*061da546Spatrick fsm.memory.append (fsm.input_symbol) 279*061da546Spatrick 280*061da546Spatrickdef BuildNumber (fsm): 281*061da546Spatrick s = fsm.memory.pop () 282*061da546Spatrick s = s + fsm.input_symbol 283*061da546Spatrick fsm.memory.append (s) 284*061da546Spatrick 285*061da546Spatrickdef EndBuildNumber (fsm): 286*061da546Spatrick s = fsm.memory.pop () 287*061da546Spatrick fsm.memory.append (int(s)) 288*061da546Spatrick 289*061da546Spatrickdef DoOperator (fsm): 290*061da546Spatrick ar = fsm.memory.pop() 291*061da546Spatrick al = fsm.memory.pop() 292*061da546Spatrick if fsm.input_symbol == '+': 293*061da546Spatrick fsm.memory.append (al + ar) 294*061da546Spatrick elif fsm.input_symbol == '-': 295*061da546Spatrick fsm.memory.append (al - ar) 296*061da546Spatrick elif fsm.input_symbol == '*': 297*061da546Spatrick fsm.memory.append (al * ar) 298*061da546Spatrick elif fsm.input_symbol == '/': 299*061da546Spatrick fsm.memory.append (al / ar) 300*061da546Spatrick 301*061da546Spatrickdef DoEqual (fsm): 302*061da546Spatrick print(str(fsm.memory.pop())) 303*061da546Spatrick 304*061da546Spatrickdef Error (fsm): 305*061da546Spatrick print('That does not compute.') 306*061da546Spatrick print(str(fsm.input_symbol)) 307*061da546Spatrick 308*061da546Spatrickdef main(): 309*061da546Spatrick 310*061da546Spatrick '''This is where the example starts and the FSM state transitions are 311*061da546Spatrick defined. Note that states are strings (such as 'INIT'). This is not 312*061da546Spatrick necessary, but it makes the example easier to read. ''' 313*061da546Spatrick 314*061da546Spatrick f = FSM ('INIT', []) 315*061da546Spatrick f.set_default_transition (Error, 'INIT') 316*061da546Spatrick f.add_transition_any ('INIT', None, 'INIT') 317*061da546Spatrick f.add_transition ('=', 'INIT', DoEqual, 'INIT') 318*061da546Spatrick f.add_transition_list (string.digits, 'INIT', BeginBuildNumber, 'BUILDING_NUMBER') 319*061da546Spatrick f.add_transition_list (string.digits, 'BUILDING_NUMBER', BuildNumber, 'BUILDING_NUMBER') 320*061da546Spatrick f.add_transition_list (string.whitespace, 'BUILDING_NUMBER', EndBuildNumber, 'INIT') 321*061da546Spatrick f.add_transition_list ('+-*/', 'INIT', DoOperator, 'INIT') 322*061da546Spatrick 323*061da546Spatrick print() 324*061da546Spatrick print('Enter an RPN Expression.') 325*061da546Spatrick print('Numbers may be integers. Operators are * / + -') 326*061da546Spatrick print('Use the = sign to evaluate and print the expression.') 327*061da546Spatrick print('For example: ') 328*061da546Spatrick print(' 167 3 2 2 * * * 1 - =') 329*061da546Spatrick inputstr = (input if PY3 else raw_input)('> ') # analysis:ignore 330*061da546Spatrick f.process_list(inputstr) 331*061da546Spatrick 332*061da546Spatrick 333*061da546Spatrickif __name__ == '__main__': 334*061da546Spatrick main() 335