Mεταγλωττιστές 4 ο εργαστηριακό μάθημα Λεξική ανάλυση και flex Σκοπός: Το μάθημα αυτό αναφέρεται: στις κανονικές εκφράσεις στην δομή και το περιεχόμενο του αρχείου-εισόδου του flex Γενικά Θεωρία Κατά την πρώτη φάση της μεταγλώττισης, ο μεταγλωττιστής διαβάζει τον πηγαίο κώδικα και μετατρέπει τις συμβολοσειρές σε tokens. Όπως είπαμε κα στο προηγούμενο εργαστήριο, ι κανονικές εκφράσεις χρησιμεύουν στην δημιουργία συγκεκριμένων προτύπων τα οποία χρησιμοποιεί ο lex να δημιουργήσει τον κατάλληλο κώδικα (στην δικιά μας περίπτωση σε C) ώστε να μπορεί να ανιχνεύει και να ταυτοποιεί κατάλληλα τις συμβολοσειρές του πηγαίου κώδικα. Η κάθε κανονική έκφραση που ορίζεται στον lex θα πρέπει να συνοδεύεται και από μια αντίστοιχη δράση (action). Συνήθως, η δράση αφορά στη επιστροφή ενός token που αντιπροσωπεύει την συμβολοσειρά που έχει αναγνωρίσει έτσι ώστε αυτό να χρησιμοποιηθεί σε μετέπειτα στάδια της μεταγλώττισης. Κανονικές εκφράσεις: αυτόματα και ειδικά σύμβολα Το ακόλουθο παράδειγμα αντιπροσωπεύει ένα απλό πρότυπο που ταυτοποιεί αναγνωριστικά (identifiers) σε μια γλώσσα προγραμματισμού. Ο lex θα το διαβάσει και θα παράγει τον αντίστοιχο κώδικα C του λεξικού αναλυτή που θα βρίσκει αναγνωριστικά στον πηγαίο κώδικα. letter(letter digit)* Πιο συγκεκριμένα, το παραπάνω πρότυπο ταυτοποιεί ακολουθίες χαρακτήρων που ξεκινούν με ένα γράμμα και στην συνέχεια ακολουθούνται από 0 ή και περισσότερα γράμματα ή αριθμητικά ψηφία (digits). Στο παράδειγμα αυτό βλέπουμε επίσης και κάποια σύμβολα όπως είναι το *, το και οι παρενθέσεις που έχουν ιδιαίτερη σημασία σε μια κανονική έκφραση. Κάθε κανονική έκφραση θα πρέπει να μπορεί να αντιστοιχηθεί σε ένα πεπερασμένο αυτόματο (finite state automaton-fsa). Τα πεπερασμένα αυτόματα αποτελούνται από καταστάσεις και μεταβάσεις μεταξύ καταστάσεων. Επιπλέον, τα πεπερασμένα αυτόματα έχουν πάντα μια αρχική 1
κατάσταση και μια ή περισσότερες τελικές καταστάσεις. Στην Εικόνα 1 φαίνεται το πεπερασμένο αυτόματο της κανονικής έκφρασης που περιγράφτηκε παραπάνω. Εικόνα 1. Το πεπερασμένο αυτόματο της έκφρασης letter(letter digit)* Η κατάσταση 0 είναι η αρχική κατάσταση του αυτόματου ενώ η 2 η τελική του κατάσταση. Καθώς διαβάζονται χαρακτήρες από τον πηγαίο κώδικα, πραγματοποιείται μετάβαση από την μια κατάσταση στην άλλη. Όταν για παράδειγμα διαβαστεί το πρώτο γράμμα, θα πραγματοποιηθεί μετάβαση από την κατάσταση 0 στην κατάσταση 1. Στην συνέχεια, και για όσο διαβάζονται ακολούθως γράμματα ή αριθμητικά ψηφία, παραμένουμε στην κατάσταση 1. Τέλος, αν διαβαστεί κάποιος άλλος χαρακτήρας περνάμε στην κατάσταση 2 που είναι και η τελική και υποδεικνύει ότι έχει αναγνωριστεί μια συμβολοσειρά από το αυτόματο. Στο Πίνακα 1, φαίνονται τα ειδικά σύμβολα που μπορούν να χρησιμοποιηθούν στην δημιουργία μιας κανονικής έκφρασης, καθώς επίσης και η ερμηνεία τους. Πίνακας 1. Τα ειδικά σύμβολα των κανονικών εκφράσεων και η ερμηνεία τους. Σύμβολο Ερμηνεία \n Αλλαγή γραμμής. Οποιοσδήποτε χαρακτήρας εκτός από την αλλαγή γραμμής * 0 ή περισσότερες επαναλήψεις της έκφρασης που προηγείται + 1 ή περισσότερες επαναλήψεις της έκφρασης που προηγείται? 0 ή 1 επανάληψη της έκφρασης που προηγείται ^ Αρχή γραμμής ή αν είναι σε μια έκφραση το πρώτο σύμβολο που εσωκλείεται σε αγκύλες σημαίνει «όχι». $ Τέλος γραμμής «ή» Παρενθέσεις Ομαδοποίηση Εισαγωγικά Αυτούσια ταυτοποίηση των χαρακτήρων που εσωκλείονται σε αυτά. [] Διάζευξη όλων των συμβόλων που εσωκλείονται στις αγκύλες. {min, max} Επανάληψη της έκφρασης που προηγείται από min έως max φορές, όπου min και max είναι ακέραιο αριθμοί. - Δηλώνει διάτημα. Π.χ. 1-5 ή a-z. Θα πρέπει πάντα το αριστερό σύμβολο του - να προηγείται από το δεξί λογικά. Π.χ. δεν μπορούμε να γράψουμε 5-2. \ Αφαιρεί την ειδική σημασία ενός συμβόλου. 2
Στον Πίνακα 2 επίσης παρουσιάζονται παραδείγματα εκφράσεων που χρησιμοποιούν τα παραπάνω σύμβολα καθώς και παραδείγματα συμβολοσειρών που ταυτοποιούν. Πίνακας 2. Παραδείγματα κανονικών εκφράσεων και η ερμηνεία τους. Έκφραση Παραδείγματα ταυτοποίησης abc AbC abc* abc+ a(bc)+ a(bc)? [abc] Abc AbC ab, abc, abcc, abccc, abcccc, abc, abcc, abccc, a, abc, abcbc, abcbcbc, a, abc a, b, c [a-z] Ένα οποιοδήποτε γράμμα από a έως και z. [a\-z] a, -, z [-az] a, -, z [A-Za-z] Ένα οποιοδήποτε γράμμα από a έως και z ή από Α έως Z. [0-9] Ένα οποιοδήποτε αριθμητικό ψηφίο από 0 έως και 9. [A-Za-z0-9] Ένα οποιοδήποτε γράμμα από a έως και z ή από A έως και Z ή ένα οποιοδήποτε αριθμητικό ψηφίο από 0-9. [ \t\n] Ένα από τα whitespaces (κενό ή tab ή αλλαγή γραμμής). ^a a μόνο αν είναι ο πρώτος χαρακτήρας της γραμμής. [^a] Οτιδήποτε εκτός από a. [^ab] Οτιδήποτε εκτός από a και b. [a^b] a, b, ^ a b a, b [a b] a, b, Α{2,4}.. \.. ΑΑ, ΑΑΑΑ Παρατήρηση! Όπως φαίνεται και από τα παραπάνω παραδείγματα τα περισσότερα από τα ειδικά σύμβολα χάνουν την ειδική τους σημασία όταν εσωκλείονται σε [ ]. Δομή αρχείου εισόδου του lex Το αρχείο (με κατάληξη.l) που θα δίνουμε σαν input στον lex θα πρέπει να χωρίζεται σε 3 μέρη, χρησιμοποιώντας σαν διαχωριστικό το διπλό %. Παρακάτω φαίνεται η δομή ενός τέτοιου αρχείου. 3
Εικόνα 2. Η δομή του αρχείου εισόδου του lex. Στο πρώτο κομμάτι του αρχείου (definitions) θα πρέπει να περιέχονται όλοι οι ορισμοί μας, είτε πρόκειται για βιβλιοθήκες ή απλές μεταβλητές που θα χρησιμοποιούμε γλώσσα C, είτε για ορισμούς κανονικών εκφράσεων. Σε περίπτωση χρήσης κώδικα σε C σε αυτό το κομμάτι του αρχείου, ο κώδικας αυτός θα πρέπει να εσωκλείεται σε %{ και %} Το δεύτερο κομμάτι του αρχείου (rules) θα περιλαμβάνει τους κανόνες των κανονικών εκφράσεων, ενώ το τρίτο (subroutines) τις συβαρτήσεις που θα χρησιμοποιούμε π.χ. main. Σαν πρώτο απλό παράδειγμα θα χρησιμοποιήσουμε το παρακάτω: %%. { printf( I found a character that is not a new line.\n ); } \n { printf( I found a new line.\n ); } %% int yywrap(){ return 1; } int main(void){ yylex(); return 1; } Στον παραπάνω κώδικα το κομμάτι definitions είναι κενό, ενώ το στο κομμάτι rules υπάρχουν 2 κανόνες. Κατά την συγγραφή των κανόνων θα πρέπει να προσέχετε να μην βάζετε περιττά κενά μπροστά από την περιγραφή τους. Αυτό σημαίνει ότι ο κάθε κανόνας θα πρέπει να ξεκινάει στην αρχή της εκάστοτε γραμμής. Αμέσως μετά από τον κάθε κανόνα θα υπάρχει η δράση (action) που θα αντιστοιχεί σε αυτόν. Το action μπορεί να περιέχει μια ή περισσότερες εντολές γραμμένες σε C τις οποίες θα εσωκλείουμε σε άγκιστρα ( { } ). Ο πρώτος κανόνας μας είναι το πρότυπο. που αντιστοιχεί σε οποιονδήποτε χαρακτήρα εκτός από την αλλαγή γραμμής. Το action που αντιστοιχεί σε αυτό το pattern υποδεικνύει ότι μόλις ενεργοποιηθεί ο συγκεκριμένος κανόνας, (μόλις δηλαδή διαβαστεί στον πηγαίο κώδικα ένας οποιοσδήποτε χαρακτήρας εκτός από την αλλαγή γραμμής) θα τυπωθεί το αντίστοιχο μήνυμα που περιγράφει η printf που ακολουθεί. Αντίστοιχα, στο δεύτερο pattern, μόλις αναγνωσθεί στον πηγαίο κώδικα αλλαγή γραμμής θα εκτυπωθεί το μήνυμα της δεύτερης printf. 4
Στο κομμάτι με τις συναρτήσεις έχουμε προσθέσει την βασική συνάρτηση main η οποία περιέχει μόνο την κλήση της συνάρτησης yylex η οποία πραγματοποιεί την λεξική ανάλυση, καθώς και την συνάρτηση yywrap η οποία επιστρέφει μη-μηδενική τιμή, υποδεικνύοντας ότι μόλις τελειώσει η ανάγνωση του πηγαίου κώδικα, η ανάλυση μπορεί να τερματιστεί. Δημιουργήστε έναν φάκελο στον δίσκο Τ του υπολογιστή σας. Τον φάκελο αυτό θα τον χρησιμοποιείτε σε κάθε ένα από τα εργαστήρια οπότε καλό θα είναι να κάθεστε κάθε φορά στον ίδιο υπολογιστή του εργαστηρίου. Αφού κατεβάσετε από την ηλεκτρονική τάξη τα Απαραίτητα αρχεία, αποσυμπιέστε το αρχικό αρχείο καθώς και κάθε ένα από τα περιεχόμενα αρχεία στον φάκελό σας. Βεβαιωθείτε ότι έχετε τροποποιήσει την μεταβλητή περιβάλλοντος path όπως περιγράφεται στο εισαγωγικό αρχείο που σας έχει δοθεί. Στην συνέχεια δημιουργείστε ένα νέο κενό αρχείο με όνομα lab1.l στον φάκελο lex που αποσυμπιέσατε. Μέσα στο αρχείο lab1.l αντιγράψτε με προσοχή τον παραπάνω κώδικα. Σώστε το αρχείο. Ανοίξτε το cmd και μπείτε χρησιμοποιώντας την εντολή cd στο path όπου έχετε αποθηκεύσει το αρχείο σας. Πληκτρολογείστε την εντολή: και πατήστε Enter. flex lab1.l Ασκήσεις Ελέγξτε για την εμφάνιση πιθανών μηνυμάτων λάθους. Στην περίπτωση που υπάρχουν λάθη θα πρέπει να τα διορθώσετε και να επαναλάβετε το προηγούμενο βήμα. Αν δεν υπάρχουν λάθη, θα πρέπει να έχει δημιουργηθεί στον φάκελό σας το αρχείο LEXYY.c. Πρόκειται για τον κώδικα του λεξικού αναλυτή σε C που δημιούργησε ο flex. Μεταγλωττίστε το αρχείο χρησιμοποιώντας την εντολή: tcc LEXYY.c και Enter. Ελέγξτε και πάλι για πιθανά μηνύματα λάθους. Εάν υπάρχουν, θα πρέπει να διορθώσετε το αρχείο lab1.l και να επαναλάβετε όλα τα προηγούμενα βήματα. Εάν δεν υπάρχουν λάθη, θα πρέπει να έχουν δημιουργηθεί στον φάκελό σας το αρχείο LEXYY.obj καθώς επίσης και το εκτελέσιμο αρχείο LEXYY.exe. Εκτελέστε το: lexyy και Enter. Στο σημείο αυτό θα δώσουμε τον κώδικα σαν είσοδο για την λεξική ανάλυση. Πληκτρολογείστε διάφορους χαρακτήρες, καθώς επίσης και Enter για να εισάγετε αλλαγή γραμμής και παρατηρήστε την αντίδραση του αναλυτή. 1. Δημιουργήστε κανονικές εκφράσεις στο αρχείο lab1.l οι οποίες να αναγνωρίζουν στον πηγαίο κώδικα τα παρακάτω. Φροντίστε ώστε να βάζετε δίπλα σε κάθε έκφραση την αντίστοιχο printf σαν action. Ελέγξτε την ορθότητα των εκφράσεών σας επαναλαμβάνοντας σε κάθε περίπτωση τα παραπάνω βήματα. 5
a. Το όνομά σας με μικρά, λατινικά γράμματα. b. Λέξεις οι οποίες να αποτελούνται από 0 ή περισσότερα αριθμητικά ψηφία από 0 έως 9. c. Λέξεις οι οποίες να αποτελούνται από 1 ή περισσότερα αριθμητικά ψηφία από 0 έως 9. d. Λέξεις οι οποίες να ξεκινούν με το λατινικό γράμμα A και στην συνέχεια από 0 ή περισσότερα αριθμητικά ψηφία από 0 έως 9. e. Λέξεις οι οποίες να ξεκινούν με το λατινικό γράμμα A ή το λατινικό γράμμα B και στην συνέχεια από 1 ή περισσότερα αριθμητικά ψηφία από 0 έως 9. f. Λέξεις οι οποίες να ξεκινούν με το λατινικό γράμμα A ή το λατινικό γράμμα B και στην συνέχεια από 0 ή 1 αριθμητικό ψηφίο από 0 έως 9. g. Λέξεις που να ξεκινάνε με οποιοδήποτε λατινικό γράμμα (κεφαλαίο ή μικρό), στην συνέχεια να έχουν ένα ή περισσότερα αριθμητικά ψηφία από 0 έως 9 και στο τέλος μια τελεία (.). h. Λέξεις που περιέχουν από 1 έως και 5 επαναλήψεις του λατινικού γράμματος f. i. Λέξεις που αποτελούνται από 5 σύμβολα, από τα οποία τα 4 πρώτα είναι οποιοσδήποτε χαρακτήρας εκτός από την αλλαγή γραμμής και το 5ο είναι το αριθμητικό ψηφίο 7. 2. Περιγράψτε τις λέξεις που αναγνωρίζονται από τις ακόλουθες κανονικές εκφράσεις. a. a(a b)*a b. (a b)*a(a b)[ab] c. a*ba*ba*ba* d. (aa bb)*[^b]((ab ba)(aa bb)*(ab ba)(aa bb))* 6