Subversion Repositories programming

Rev

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