Subversion Repositories programming

Rev

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