Subversion Repositories programming

Rev

Rev 143 | Details | Compare with Previous | Last modification | View Log | RSS feed

Rev Author Line No. Line
130 ira 1
#!/usr/bin/env python
2
# Copyright: Ira W. Snyder
3
# Start Date: 2005-10-08
143 ira 4
# End Date: 2005-10-24
130 ira 5
# License: Public Domain
6
#
7
# Changelog Follows:
131 ira 8
# - 2005-10-16
9
# - Implemented the nfa class. It's mostly complete at this point.
10
# - E() function works for circular loops now.
11
# - Made the nfa.__next_states() function always return a valid reference
12
#   to a list in the dictionary. This means you should NEVER use
13
#   self.trans_func[(state, letter)] in code anywhere now :)
14
# - Final states are now checked for validity.
130 ira 15
#
140 ira 16
# - 2005-10-19
17
# - Everything now works, with a menu even.
18
#
19
# - 2005-10-20
20
# - Added the check for <Python-2.3 compatibility.
21
# - Commented the source more.
22
#
142 ira 23
# - 2005-10-22
151 ira 24
# - Removed an unnecessary call in input_automaton()
142 ira 25
#
143 ira 26
# - 2005-10-24
27
# - Realized that I need to call E(states) after I find out where I'm going.
28
#   The program wasn't working at all before, now it is. See class notes from
29
#   2005-10-24 for some examples that weren't working.
30
# - Made 'True' and 'False' print correctly even if we are on pre-boolean
31
#   version of python.
32
# - Added a 'Verbose Mode' toggle to the menu. This makes the outputted DFA
33
#   table have more information: the NFA Equiv. State
34
# - It's ready to be turned in now :)
35
#
130 ira 36
 
37
################################################################################
38
# IMPORTANT NOTES
39
################################################################################
140 ira 40
# The DFA table format:
41
#
42
# [ [Final, 0, NFA_Equivalent, letter1, letter2, ..., lettern],
43
# [ [Final, 1, NFA_Equivalent, letter1, letter2, ..., lettern],
44
# [ [Final, 2, NFA_Equivalent, letter1, letter2, ..., lettern] ]
45
#
130 ira 46
################################################################################
140 ira 47
# The nfa.trans_func format:
48
#
49
# dict[(state, letter)] = [list, of, states]
50
#
51
################################################################################
130 ira 52
 
140 ira 53
# Check for <Python-2.3 compatibility (boolean values)
54
try:
55
  True, False
56
except NameError:
57
  (True, False) = (1, 0)
58
 
130 ira 59
import sys, string
60
 
61
class nfa:
62
    """Implements a Non-Deterministic Finite Automaton"""
63
 
64
    def __init__(self):
140 ira 65
        """Constructor for the nfa object"""
138 ira 66
        self.__clear()
143 ira 67
        self.verbose = False
138 ira 68
 
69
    def __clear(self):
140 ira 70
        """Clear all instance variables of the nfa class"""
130 ira 71
        self.trans_func = {}
72
        self.num_states = 0
73
        self.final_states = []
74
        self.initial_state = 0
136 ira 75
        self.possible_letters = []
76
        self.dfa_table = []
131 ira 77
 
130 ira 78
    def __state_in_range(self, state):
79
        """Check if a given state is in the acceptable range"""
80
 
140 ira 81
        # Check to make sure this can be converted to an int.
82
        # Return False if the conversion fails.
130 ira 83
        try:
84
            temp = int(state)
85
        except (TypeError, ValueError):
86
            return False
87
 
140 ira 88
        # Make sure this is a state in the range
130 ira 89
        if temp >= 0 and temp < self.num_states:
90
            return True
91
 
140 ira 92
        # We weren't in the correct range, so return False
130 ira 93
        return False
131 ira 94
 
130 ira 95
    def __input_num_states(self):
140 ira 96
        """Ask the user (nicely) for the number of states this automaton will have"""
130 ira 97
 
98
        done = False
99
        while not done:
100
            num_states = raw_input('How many states in this automaton: ')
138 ira 101
            print
130 ira 102
 
140 ira 103
            # Make sure we can convert the value successfully, then set
104
            # the num_states variable.
130 ira 105
            try:
106
                self.num_states = int(num_states)
107
                done = True
108
            except (TypeError, ValueError):
109
                print 'Bad input, not an integer, try again'
110
                print
111
 
112
    def __input_final_states(self):
140 ira 113
        """Ask the user for a list of final states for this automaton"""
130 ira 114
 
151 ira 115
        print 'Enter final states on one line, separated by spaces'
131 ira 116
 
130 ira 117
        done = False
118
        while not done:
119
            final_states = raw_input('Final states: ')
138 ira 120
            print
131 ira 121
 
140 ira 122
            # Check each value entered, one by one, and set bad_data if there
123
            # was a state out of the valid range
130 ira 124
            bad_data = False
125
            for i in final_states.split():
126
                if not self.__state_in_range(i):
127
                    print 'State %s out of range. All input discarded.' % (i, )
128
                    print 'Try again please'
129
                    print
130
                    bad_data = True
131
                    break
132
 
140 ira 133
            # If we left the for loop with bad data, start over.
130 ira 134
            if bad_data:
135
                continue
136
 
140 ira 137
            # All data is good, read the values into the final_states list.
130 ira 138
            for i in final_states.split():
131 ira 139
                if self.__state_in_range(i):
140
                    self.final_states.append(int(i))
130 ira 141
 
142
            done = True
143
 
144
    def __input_all_edges(self):
140 ira 145
        """Ask the user to input all of the edges for this automaton"""
130 ira 146
 
147
        print 'Edges take the form "from HERE, by LETTER, to THERE"'
140 ira 148
        print 'Enter something like: "1 a 3" to represent the following'
149
        print 'from q1, by a, go to q3'
130 ira 150
        print
140 ira 151
        print 'RULES:'
152
        print '------------------------------------------------------'
153
        print '1. Enter a blank line to stop reading edges'
130 ira 154
        print '2. ^ is the empty string'
140 ira 155
        print '3. Enter one letter at a time (no multi-letter states)'
138 ira 156
        print
130 ira 157
 
140 ira 158
        # Read edges, one by one. Check them as they are entered.
130 ira 159
        done = False
160
        while not done:
161
            edge = raw_input('Edge: ')
162
 
140 ira 163
            # Check to see if we got a blank line
130 ira 164
            if len(edge) == 0:
165
                done = True
166
                continue
131 ira 167
 
140 ira 168
            # If we didn't get exactly 3 arguments, try again
130 ira 169
            if len(edge.split()) != 3:
140 ira 170
                print 'Bad edge entered.'
171
                print 'Exactly 3 arguments are required for a valid edge.'
130 ira 172
                print
173
                continue
131 ira 174
 
140 ira 175
            # All checks appear fine, set a variable for each piece
130 ira 176
            (cur_st, letter, next_st) = edge.split()
177
 
131 ira 178
            # Make sure the states entered are in range
130 ira 179
            if self.__state_in_range(cur_st) and self.__state_in_range(next_st):
180
                new_state = False
140 ira 181
                cur_st = int(cur_st)    # convert to int
182
                next_st = int(next_st)  # convert to int
130 ira 183
 
131 ira 184
                # Add the state to the list if it's not there already
185
                if next_st not in self.__next_states(cur_st, letter):
186
                    self.__next_states(cur_st, letter).append(next_st)
130 ira 187
 
140 ira 188
            else: # At least one state was not in range
130 ira 189
                print 'Invalid current or next state entered'
190
                print
191
                continue
131 ira 192
 
130 ira 193
    def __input_automaton(self):
194
        """Read this entire automaton's input"""
195
 
196
        self.__input_num_states()
197
        self.__input_final_states()
198
        self.__input_all_edges()
199
 
200
    def main_menu(self):
140 ira 201
        """Display the main menu for this automaton"""
202
 
138 ira 203
        done = False
204
        automaton_entered = False
131 ira 205
 
138 ira 206
        while not done:
207
            print 'Menu:'
208
            print '========================================'
209
            print '1. Enter an NFA'
210
            print '2. Output corresponding DFA'
143 ira 211
            print '3. %s Verbose Mode' % (self.verbose and 'Disable' or 'Enable', )
212
            print '4. Quit'
138 ira 213
            print
214
            s = raw_input('Choice >>> ')
215
            print
130 ira 216
 
138 ira 217
            if s == '1':
218
                self.__clear()
219
                self.__input_automaton()
220
                automaton_entered = True
221
                print
222
            elif s == '2':
223
                if automaton_entered:
224
                    self.__output_dfa()
225
                else:
226
                    print 'Enter a NFA first'
227
                print
228
            elif s == '3':
143 ira 229
                self.verbose = not self.verbose
230
                print 'Verbose Mode %s' % (self.verbose and 'Enabled' or 'Disabled', )
231
                print
232
            elif s == '4':
138 ira 233
                done = True
234
            else:
235
                print 'Bad Selection'
236
                print
237
 
238
    def __output_dfa(self):
140 ira 239
        """Generate and print the DFA that corresponds to the NFA entered"""
240
 
131 ira 241
        print
136 ira 242
        print 'Initial State: %s' % (0, )
243
        print
244
        self.__generate_dfa_table()
137 ira 245
        self.__print_dfa_table()
138 ira 246
        print
131 ira 247
 
137 ira 248
    def __print_dfa_table(self):
140 ira 249
        """Print out a nicely spaced representation of the DFA's table"""
250
 
143 ira 251
        if self.verbose:
252
            header1 = '%-8s%-8s%-20s' % ('Final', 'Number', 'NFA Equiv. State')
253
        else:
254
            header1 = '%-8s%-8s' % ('Final', 'State #')
255
 
137 ira 256
        header2 = ''
136 ira 257
 
137 ira 258
        for l in self.possible_letters:
259
            header2 = '%s%-4s' % (header2, l)
260
 
261
        heading = '%s %s' % (header1, header2)
262
        hdr_line = ''
138 ira 263
 
137 ira 264
        for i in range(len(heading)):
265
            hdr_line = '%s-' % (hdr_line, )
266
 
267
        print heading
268
        print hdr_line
269
 
270
        for rec in self.dfa_table:
143 ira 271
            if self.verbose:
272
                line1 = '%-8s%-8s%-20s' % (rec[0] and 'True' or 'False', rec[1], rec[2])
273
            else:
274
                line1 = '%-8s%-8s' % (rec[0] and 'True' or 'False', rec[1])
275
 
137 ira 276
            line2 = ''
138 ira 277
 
137 ira 278
            for l in range(len(self.possible_letters)):
279
                line2 = '%s%-4s' % (line2, rec[l+3])
280
 
281
            print '%s %s' % (line1, line2)
282
 
130 ira 283
    def __next_states(self, state, letter):
131 ira 284
        """Return the next states for the key (state, letter)"""
285
 
130 ira 286
        try:
287
            next = self.trans_func[(state, letter)]
288
        except KeyError:
131 ira 289
            # Create a new empty list for this state if it doesn't exist yet
290
            next = self.trans_func[(state, letter)] = []
130 ira 291
 
140 ira 292
        # Make sure that we can go to ourselves by the empty letter
131 ira 293
        if letter == '^' and state not in next:
294
            next.append(state)
295
 
130 ira 296
        return next
131 ira 297
 
136 ira 298
    def __E(self, state, letter='^', storage=None, visited=None):
131 ira 299
        """Calculate E(state) and return it. This handles circular E()
300
        calculations."""
301
 
302
        # Work around weird mutable default argument stuff
303
        if storage is None:
304
            storage = []
305
 
306
        # Work around weird mutable default argument stuff
307
        if visited is None:
308
            visited = []
309
 
310
        # Find all of the direct next states that we can get to by
311
        # the empty string, and append anything that is not already there
136 ira 312
        for i in self.__next_states(state, letter):
131 ira 313
            if i not in storage:
314
                storage.append(i)
130 ira 315
 
131 ira 316
        # Visit everything in storage that has not been visited already
317
        for i in storage:
318
            if i not in visited:
130 ira 319
                visited.append(i)
136 ira 320
                temp = self.__E(i, letter, storage, visited)
130 ira 321
 
131 ira 322
                # Avoid duplicating things in storage
323
                for j in temp:
324
                    if j not in storage:
325
                        storage.append(j)
326
 
130 ira 327
        return storage
131 ira 328
 
135 ira 329
    def __unique_sort(self, li):
330
        """Make sure everything in a list is unique, and then sort it"""
331
 
332
        newlist = []
333
 
334
        for i in li:
335
            if i not in newlist:
336
                newlist.append(i)
337
 
338
        newlist.sort()
339
        return newlist
340
 
136 ira 341
    def __is_final_state(self, state):
342
        """Check if a state is a final state."""
343
 
344
        if state in self.final_states:
345
            return True
346
 
347
        return False
348
 
349
 
350
    def __is_list_final(self, states):
351
        """Check if at least one state in the list "states" is final"""
352
 
353
        for s in states:
354
            if self.__is_final_state(s):
355
                return True
356
 
357
        return False
358
 
359
    def __get_temp_record(self, num_letters):
140 ira 360
        """Create a record (for the DFA table) that has the proper number
361
        of slots"""
362
 
136 ira 363
        blank = None
364
        return [blank for i in range(num_letters + 3)]
365
 
138 ira 366
    def __get_possible_letters(self):
140 ira 367
        """Create a list of all the possible letters for the NFA,
368
        and store it in possible_letters"""
369
 
370
        # Get the list of all letters
135 ira 371
        possible_letters = []
372
 
373
        for (state, letter) in self.trans_func.keys():
137 ira 374
            if letter not in possible_letters and letter != '^':
135 ira 375
                possible_letters.append(letter)
376
 
138 ira 377
        possible_letters = self.__unique_sort(possible_letters)
136 ira 378
        self.possible_letters = possible_letters
140 ira 379
 
138 ira 380
    def __generate_dfa_table(self):
140 ira 381
        """Generate a table for the DFA representation of the NFA entered earlier"""
135 ira 382
 
140 ira 383
        # Get all the possible letters
138 ira 384
        self.__get_possible_letters()
385
 
140 ira 386
        # Prime the dfa table with the first state
136 ira 387
        self.make_basic_record(self.__E(0))
135 ira 388
 
140 ira 389
        # Loop until we resolve every letter in the table
390
        while self.find_unresolved_letter() != (-1, -1):
136 ira 391
            (record, letter_pos) = self.find_unresolved_letter()
392
            self.resolve_letter(record, letter_pos)
135 ira 393
 
136 ira 394
    def resolve_letter(self, record, letter_pos):
140 ira 395
        """Resolve a letter in the table, either adding a new entry if the
396
        required state doesn't already exist, or putting a link to
397
        an existing state"""
398
 
399
        # Get the NFA equivalent multi-state
136 ira 400
        fromstates = self.dfa_table[record][2]
401
        tostates = []
135 ira 402
 
143 ira 403
        # Find all the states we can go to from our multi-state.
404
        # It is _EXTREMELY_ important that you call E(s) for every state that
405
        # you can get to, otherwise you miss certain situations.
406
        for from_s in fromstates:
407
            for next_s in self.__next_states(from_s, self.possible_letters[letter_pos-3]):
408
                tostates.extend(self.__E(next_s))
135 ira 409
 
140 ira 410
        tostates = self.__unique_sort(tostates)
411
 
412
        # Make the letter we are trying to resolve point to a record in the table.
413
        # make_basic_record() will return the correct record, even if it needs to
414
        # create a new one.
136 ira 415
        self.dfa_table[record][letter_pos] = self.make_basic_record(tostates)
135 ira 416
 
140 ira 417
    def find_unresolved_letter(self):
136 ira 418
        """Returns an index pair of an unresolved letter that exists in the dfa_table.
419
        If there are no more letters, return (-1, -1)"""
135 ira 420
 
136 ira 421
        for i in range(len(self.dfa_table)):
422
            for j in range(len(self.possible_letters)):
423
                if self.dfa_table[i][j+3] == None:
424
                    return (i, j+3)
135 ira 425
 
136 ira 426
        return (-1, -1)
135 ira 427
 
136 ira 428
    def make_basic_record(self, from_states):
140 ira 429
        """Create a record, if necessary, for the states "from_states."
430
        If a corresponding state already exists, return it's index. A new state
431
        will have everything but the letters filled in."""
135 ira 432
 
136 ira 433
        if self.dfa_state_exists(from_states) == -1: #doesn't exist
434
            temp_record = self.__get_temp_record(len(self.possible_letters))
435
            recordnum = len(self.dfa_table)
135 ira 436
 
136 ira 437
            temp_record[1] = recordnum
438
            temp_record[2] = self.__unique_sort(from_states)
439
            temp_record[0] = self.__is_list_final(from_states)
135 ira 440
 
136 ira 441
            self.dfa_table.append(temp_record)
135 ira 442
 
136 ira 443
        # always return a re-call of the function. This will not fail, since a new
444
        # record is added above if a record does not already exist.
445
        return self.dfa_state_exists(from_states)
135 ira 446
 
447
 
136 ira 448
    def dfa_state_exists(self, from_states):
449
        """Find out if this state already exists in the dfa table.
450
        If it does exist, return it's dfa state name.
451
        If it does not exist, return -1"""
130 ira 452
 
136 ira 453
        sorted_states = self.__unique_sort(from_states)
454
 
455
        for i in range(len(self.dfa_table)):
456
            if self.dfa_table[i][2] == sorted_states:
457
                return self.dfa_table[i][1]
458
 
459
        return -1
460
 
130 ira 461
if __name__ == '__main__':
462
    automaton = nfa()
463
    automaton.main_menu()
464