Υλοποίηση αυτομάτων DFA μέσω Python¶
Στα παραδείγματα και τις ασκήσεις θα χρησιμοποιήσουμε την έτοιμη κλάση DFA
από τις βιβλιοθήκες Python του εργαστηρίου. Η κλάση αυτή παρέχει ένα απλό interface για την κατασκευή και λειτουργία αυτομάτων DFA.
Κατασκευή και χρήση αυτομάτου DFA¶
Αρχικά κάνουμε import την κλάση DFA
στο πρόγραμμά μας:
from compilerlabs import DFA
Αμέσως μετά δημιουργούμε ένα αυτόματο, στιγμιότυπο της κλάσης DFA
:
test_dfa = DFA('s0')
Το υποχρεωτικό όρισμα στην DFA()
είναι το όνομα της αρχικής κατάστασης του αυτομάτου (εδώ s0
).
Στη συνέχεια, μπορούμε να ορίσουμε μεταβάσεις του αυτομάτου μέσω της μεθόδου transition(fromstate,next_input,tostate)
όπου fromstate
είναι η κατάσταση απ’ όπου, με την εμφάνιση του χαρακτήρα next_input
, μεταβαίνουμε στη νέα κατάσταση tostate
, π.χ.:
test_dfa.transition('s0','t','s1')
που σημαίνει ότι όταν βρισκόμαστε στην κατάσταση s0
και εμφανιστεί ο χαρακτήρας t
θα μεταβούμε στην κατάσταση s1
.
Μπορούμε ακόμα να δώσουμε μια λίστα χαρακτήρων για μετάβαση, π.χ.
test_dfa.transition('s1',['0','1','2'],'s2')
που δηλώνει μετάβαση από το s1
στο s2
με έναν οποιονδήποτε χαρακτήρα από τους 0
, 1
ή 2
. Προσέξε ότι το προηγούμενο είναι ισοδύναμο με το:
test_dfa.transition('s1','0','s2')
test_dfa.transition('s1','1','s2')
test_dfa.transition('s1','2','s2')
Η χρήση της μεθόδου accept(state,token)
είναι αυτή που ορίζει μια κατάσταση ως κατάσταση αποδοχής. Μέσω της accept
ορίζουμε το σύμβολο (token) που επιστρέφεται:
test_dfa.accept('s2','TOK1')
Στο προηγούμενο παράδειγμα η s2
ορίζεται ως κατάσταση αποδοχής που αναγνωρίζει και επιστρέφει το σύμβολο TOK1
.
Όταν ολοκληρώσουμε τον ορισμό των μεταβάσεων και τις καταστάσεις αποδοχής του αυτομάτου, μπορούμε να το χρησιμοποιήσουμε για να ελέγξουμε ταιριάσματα σε strings. Η μέθοδος που παρέχεται ονομάζεται scan(text)
, όπου text
είναι το string που θα προσπαθήσουμε να ταιριάξουμε:
token,lexeme = test_dfa.scan('t2')
Η μέθοδος επιστρέφει το token που αναγνωρίστηκε (ή None αν δεν υπάρχει ταίριασμα) και το αντίστοιχο κείμενο (ή το κενό string αν δεν αναγνωρίστηκε κάτι).
Παράδειγμα¶
Στη συνέχεια θα δούμε την υλοποίηση του γνωστού παραδείγματος του αυτομάτου με τα τρία keywords, τις λέξεις long, term και test του επόμενου σχήματος:
Η υλοποίηση μέσω της κλάσης DFA
:
from compilerlabs import DFA
dfa1 = DFA('s0')
dfa1.transition('s0','t','s1')
dfa1.transition('s0','l','s2')
dfa1.transition('s1','e','s3')
dfa1.transition('s3','s','s4')
dfa1.transition('s4','t','s5')
dfa1.transition('s3','r','s6')
dfa1.transition('s6','m','s7')
dfa1.transition('s2','o','s8')
dfa1.transition('s8','n','s9')
dfa1.transition('s9','g','s10')
dfa1.accept('s5','TEST_TOKEN')
dfa1.accept('s7','TERM_TOKEN')
dfa1.accept('s10','LONG_TOKEN')
token,lexeme = dfa1.scan('long')
print(token,lexeme) # prints LONG_TOKEN long
token,lexeme = dfa1.scan('term')
print(token,lexeme) # prints TERM_TOKEN term
token,lexeme = dfa1.scan('test')
print(token,lexeme) # prints TEST_TOKEN test
Ομαδοποίηση χαρακτήρων¶
Ας εξετάσουμε την υλοποίηση του αυτομάτου DFA το οποίο αναγνωρίζει ακολουθίες ψηφίων 0..9 οποιουδήποτε μήκους:
Για να περιγράψουμε τη μετάβαση από το \(s_0\) στο \(s_1\) θα μπορούσαμε να γράψουμε:
num_dfa.transition('s0',['0','1','2','3','4','5','6','7','8','9',],'s1')
Για να διευκολύνει την κατασκευή, το interface της κλάσης DFA
επιτρέπει την προαιρετική χρήση ενός «προ-επεξεργαστή» (preprocessor) των χαρακτήρων εισόδου, ο οποίος μπορεί να ομαδοποιήσει διάφορους χαρακτήρες σε μια και μόνο αναγνωριστική ετικέτα. Στο παράδειγμα του αυτομάτου των αριθμητικών ψηφίων μπορούμε να ορίσουμε:
def preproc(c):
if c>='0' and c<='9':
return 'DIGIT'
return c
Το όνομα της συνάρτησης του preprocessor περνάει ως δεύτερο προαιρετικό όρισμα στην κατασκευή του DFA:
dfa2 = DFA('s0',preproc)
Έχοντας ορίσει τον preprocessor της εισόδου όπως πριν, το αυτόματο μπορεί να κατασκευαστεί ως εξής:
dfa2.transition('s0','DIGIT','s1')
dfa2.transition('s1','DIGIT','s1')
dfa2.accept('s1','NUMBER_TOKEN')
Παράδειγμα αναζήτησης:
token,lexeme = dfa2.scan('123')
print(token,lexeme) # prints NUMBER_TOKEN 123
Διαφορές από το θεωρητικό αυτόματο¶
Το θεωρητικό αυτόματο (D)FA θα προσπαθήσει να ταιριάξει όλους τους χαρακτήρες εισόδου. Εάν για κάποιον χαρακτήρα δεν υπάρχει μετάβαση από την τρέχουσα κατάσταση θα παραχθεί σφάλμα. Το θεωρητικό αυτόματο αναγνωρίζει ένα κείμενο όταν, αφού το καταναλώσει όλο, βρίσκεται σε κατάσταση αποδοχής.
Για παράδειγμα, το θεωρητικό αυτόματο που αναγνωρίζει σειρές από αριθμητικά ψηφία (0..9) στο προηγούμενο σχήμα θα επιστρέψει σφάλμα τόσο στο a123
(επειδή από την αρχική κατάσταση δεν υπάρχει μετάβαση με τον χαρακτήρα a
) όσο και στο 123a
(γιατί από το s1
δεν υπάρχει μετάβαση με το a
).
Η λειτουργία του θεωρητικού αυτομάτου εξυπηρετεί πρακτικά μόνο αν θέλουμε να ελέγξουμε την εγκυρότητα όλου του κειμένου εισόδου (input validation).
Υπάρχουν όμως και άλλες πιθανές πρακτικές χρήσεις ενός αυτομάτου, όπως:
Αναζήτηση (search): εδώ ψάχνουμε για την εμφάνιση ενός σχεδίου κειμένου μέσα σε ένα μεγαλύτερο κείμενο. Π.χ. στην είσοδο
123a
θα θέλαμε να αναγνωρίσουμε το123
μόνο.Μετατροπή σε σύμβολα (tokenization): η κλασσική λειτουργία στο πρώτο βήμα ενός μεταγλωττιστή, όπου επαναληπτικά αναγνωρίζουμε σχέδια κειμένου σε ένα μεγαλύτερο κείμενο. Π.χ. από το
int k;
να αναγνωρίσουμε τα σύμβολα (tokens)KEYWORD_INT
,SPACE
,IDENTIFIER
καιSEMICOLON
.
Για να επιτρέψει την υλοποίηση των παραπάνω, το αυτόματο της κλάσης DFA
που χρησιμοποιούμε στο εργαστήριο λειτουργεί ως εξής:
Εάν δεν είναι δυνατή η μετάβαση από την τρέχουσα κατάσταση με τον χαρακτήρα εισόδου, ελέγχεται εάν η κατάσταση αυτή είναι κατάσταση αποδοχής.
Εάν ναι, τότε αναγνωρίζεται το κείμενο μέχρι το σημείο αυτό (μέρος δηλαδή του συνολικού κειμένου).
Εάν όχι, παράγεται σφάλμα.
Στο παράδειγμα του αυτομάτου των αριθμητικών ψηφίων, η κλάση DFA
θα αναγνωρίσει με επιτυχία το μέρος 123
της εισόδου 123a
.
Προαιρετικά ταιριάσματα¶
Έστω ότι υλοποιείται το αυτόματο της επόμενης εικόνας:
Ο στόχος είναι η αναγνώριση ακεραίων αριθμών (στην κατάσταση \(s_1\)) και αριθμών με δεκαδικό μέρος (στην κατάσταση αποδοχής \(s_3\)).
Εάν το string εισόδου είναι το '123.'
, θα θέλαμε να αναγνωρίσουμε το 123
ως ακέραιο. Κάτι τέτοιο δεν θα συμβεί όμως κανονικά, γιατί στο τέλος της αναζήτησης βρισκόμαστε στην κατάσταση \(s_2\), η οποία δεν είναι κατάσταση αποδοχής.
Για να διορθωθεί αυτή η ατέλεια έχει εισαχθεί στη scan()
η δυνατότητα μνήμης, όπου φυλάσσεται η τελευταία κατάσταση αποδοχής όπου έχει βρεθεί το αυτόματο. Αυτή η κατάσταση (αν υπάρχει) επιστρέφεται όταν δεν μπορούμε να προχωρήσουμε άλλο.
Το αυτόματο για την αναγνώριση int και float αριθμών μπορεί να υλοποιηθεί με την κλάση DFA
ως εξής:
def preproc(c):
if c>='0' and c<='9':
return 'DIGIT'
return c
dfa3 = DFA('s0',preproc)
dfa3.transition('s0','DIGIT','s1')
dfa3.transition('s1','DIGIT','s1')
dfa3.transition('s1','.','s2')
dfa3.transition('s2','DIGIT','s3')
dfa3.transition('s3','DIGIT','s3')
dfa3.accept('s1','INT_TOKEN')
dfa3.accept('s3','FLOAT_TOKEN')
token,lexeme = dfa3.scan('123')
print(token,lexeme) # prints INT_TOKEN 123
token,lexeme = dfa3.scan('123.')
print(token,lexeme) # prints INT_TOKEN 123
token,lexeme = dfa3.scan('123.4')
print(token,lexeme) # prints FLOAT_TOKEN 123.4
Ασκήσεις¶
Υλοποιήστε μέσω της κλάσης
DFA
αυτόματο για την αναγνώριση ακέραιων και δεκαδικών αριθμών οποιουδήποτε μήκους στις εξής μορφές:
1234
(επιστρεφόμενο σύμβολο:INT_TOKEN
)
123.48
(επιστρεφόμενο σύμβολο:FLOAT_TOKEN
)
123.48e56
(επιστρεφόμενο σύμβολο:SCIENTIFIC_TOKEN
)
Υλοποιήστε μέσω μέσω της κλάσης
DFA
αυτόματο για την αναγνώριση έγκυρων μορφών 24ωρης ώρας (00:00
έως23:59
).
Η ώρα μπορεί να εκφράζεται με ένα ή δύο ψηφία (π.χ.
3:45
αλλά και03:45
).Τα λεπτά θα έχουν πάντα δύο ψηφία.
Κάθε άκυρη μορφή ώρας δεν πρέπει να αναγνωρίζεται (π.χ.
31:13
ή9:72
).
Υλοποιήστε μέσω μέσω της κλάσης
DFA
αυτόματο για την αναγνώριση αριθμών μητρώου φοιτητών στη μορφήΠ2013000
για τα έτη 2004 έως 2020.
Προηγείται το γράμμα
Π
.Ακολουθεί το έτος με τέσσερα ψηφία (2004 έως και 2020).
Στο τέλος είναι ο αριθμός του φοιτητή με τρία ψηφία.