Προγραμματισμός

Λεξική ανάλυση, Μέρος 2: Δημιουργήστε μια εφαρμογή

Τον περασμένο μήνα κοίταξα τα μαθήματα που παρέχει η Java για να κάνει βασική λεξική ανάλυση. Αυτό το μήνα θα διαβάσω μια απλή εφαρμογή που χρησιμοποιεί StreamTokenizer για να εφαρμόσετε μια διαδραστική αριθμομηχανή.

Για να ελέγξετε εν συντομία το άρθρο του περασμένου μήνα, υπάρχουν δύο κλάσεις λεξικών αναλυτών που περιλαμβάνονται στην τυπική κατανομή Java: StringTokenizer και StreamTokenizer. Αυτοί οι αναλυτές μετατρέπουν την είσοδό τους σε διακριτά tokens που ένας αναλυτής μπορεί να χρησιμοποιήσει για να κατανοήσει μια δεδομένη είσοδο. Ο αναλυτής εφαρμόζει μια γραμματική, η οποία ορίζεται ως μία ή περισσότερες καταστάσεις στόχων που επιτυγχάνονται βλέποντας διάφορες ακολουθίες διακριτικών. Όταν επιτευχθεί η κατάσταση στόχου ενός αναλυτή, εκτελεί κάποια ενέργεια. Όταν ο αναλυτής εντοπίσει ότι δεν υπάρχουν πιθανές καταστάσεις στόχων, δεδομένης της τρέχουσας ακολουθίας διακριτικών, το ορίζει ως κατάσταση σφάλματος. Όταν ένας αναλυτής φτάσει σε κατάσταση σφάλματος, εκτελεί μια ενέργεια ανάκτησης, η οποία επαναφέρει τον αναλυτή σε ένα σημείο στο οποίο μπορεί να αρχίσει να αναλύει ξανά. Συνήθως, αυτό εφαρμόζεται με την κατανάλωση διακριτικών έως ότου ο αναλυτής επιστρέψει σε ένα έγκυρο σημείο εκκίνησης.

Τον περασμένο μήνα σας έδειξα μερικές μεθόδους που χρησιμοποίησαν ένα StringTokenizer για την ανάλυση ορισμένων παραμέτρων εισαγωγής. Αυτό το μήνα θα σας δείξω μια εφαρμογή που χρησιμοποιεί ένα StreamTokenizer αντικείμενο να αναλύσει μια ροή εισόδου και να εφαρμόσει μια διαδραστική αριθμομηχανή.

Δημιουργία εφαρμογής

Το παράδειγμά μας είναι μια διαδραστική αριθμομηχανή που είναι παρόμοια με την εντολή Unix bc (1). Όπως θα δείτε, ωθεί το StreamTokenizer κατηγορίας μέχρι την άκρη της χρησιμότητάς του ως λεξικού αναλυτή. Έτσι, χρησιμεύει ως μια καλή επίδειξη του πού μπορεί να σχεδιαστεί η γραμμή μεταξύ "απλών" και "σύνθετων" αναλυτών. Αυτό το παράδειγμα είναι μια εφαρμογή Java και επομένως λειτουργεί καλύτερα από τη γραμμή εντολών.

Ως γρήγορη περίληψη των ικανοτήτων του, η αριθμομηχανή δέχεται εκφράσεις στη φόρμα

[όνομα μεταβλητής] "=" έκφραση 

Το όνομα της μεταβλητής είναι προαιρετικό και μπορεί να είναι οποιαδήποτε σειρά χαρακτήρων στο προεπιλεγμένο εύρος λέξεων. (Μπορείτε να χρησιμοποιήσετε τη μικροεφαρμογή γυμναστικής από το άρθρο του περασμένου μήνα για να ανανεώσετε τη μνήμη σας σε αυτούς τους χαρακτήρες.) Εάν παραλείπεται το όνομα της μεταβλητής, τυπώνεται απλά η τιμή της έκφρασης Εάν υπάρχει το όνομα της μεταβλητής, η τιμή της έκφρασης αντιστοιχεί στη μεταβλητή. Μόλις εκχωρηθούν μεταβλητές, μπορούν να χρησιμοποιηθούν σε μεταγενέστερες εκφράσεις. Έτσι, γεμίζουν το ρόλο των «αναμνήσεων» σε μια σύγχρονη φορητή αριθμομηχανή.

Η έκφραση αποτελείται από τελεστές με τη μορφή αριθμητικών σταθερών (διπλής ακρίβειας, σταθερών κινητής υποδιαστολής) ή μεταβλητών ονομάτων, τελεστών και παρενθέσεων για την ομαδοποίηση συγκεκριμένων υπολογισμών. Οι νόμιμοι τελεστές είναι προσθήκη (+), αφαίρεση (-), πολλαπλασιασμός (*), διαίρεση (/), bitwise AND (&), bitwise OR (|), bitwise XOR (#), exponentiation (^) και unary negation είτε με το μείον (-) για το αποτέλεσμα συμπληρώματος δύο είτε το bang (!) για το αποτέλεσμα συμπληρώματος.

Εκτός από αυτές τις δηλώσεις, η εφαρμογή της αριθμομηχανής μπορεί επίσης να λάβει μία από τις τέσσερις εντολές: "dump," "clear", "help" και "quit." ο εγκαταλείπω Η εντολή εκτυπώνει όλες τις μεταβλητές που έχουν καθοριστεί, καθώς και τις τιμές τους. ο Σαφή Η εντολή διαγράφει όλες τις τρέχουσες καθορισμένες μεταβλητές. ο βοήθεια Η εντολή εκτυπώνει μερικές γραμμές κειμένου βοήθειας για να ξεκινήσει ο χρήστης. ο εγκαταλείπω εντολή προκαλεί την έξοδο της εφαρμογής.

Το συνολικό παράδειγμα εφαρμογής αποτελείται από δύο αναλυτές - έναν για εντολές και δηλώσεις και έναν για εκφράσεις.

Δημιουργία προγράμματος ανάλυσης εντολών

Το πρόγραμμα ανάλυσης εντολών εφαρμόζεται στην κλάση εφαρμογών για το παράδειγμα STExample.java. (Ανατρέξτε στην ενότητα Πόροι για δείκτη στον κωδικό.) Το κύριος Η μέθοδος για αυτήν την τάξη ορίζεται παρακάτω. Θα περάσω τα κομμάτια για σένα.

 1 public static void main (String args []) ρίχνει IOException {2 Hashtable μεταβλητές = νέο Hashtable (); 3 StreamTokenizer st = νέο StreamTokenizer (System.in); 4 st.eolIsSignificant (true); 5 st.lowerCaseMode (true); 6η έκτακτη χρέωση ('/'); 7η έκτακτη χρέωση ('-'); 

Στον κώδικα πάνω από το πρώτο πράγμα που κάνω είναι να διαθέσω ένα java.util.Hashtable κλάση για να κρατήσει τις μεταβλητές. Μετά από αυτό διαθέτω ένα StreamTokenizer και προσαρμόστε το ελαφρώς από τις προεπιλογές του. Το σκεπτικό για τις αλλαγές έχουν ως εξής:

  • eolIsSignificant Έχει οριστεί αληθής έτσι ώστε το tokenizer να επιστρέψει μια ένδειξη τέλους γραμμής. Χρησιμοποιώ το τέλος της γραμμής ως το σημείο όπου τελειώνει η έκφραση.

  • lowerCaseMode Έχει οριστεί αληθής έτσι ώστε τα ονόματα των μεταβλητών να επιστρέφονται πάντα με πεζά. Με αυτόν τον τρόπο, τα ονόματα των μεταβλητών δεν είναι πεζά.

  • Ο χαρακτήρας κάθετος (/) έχει οριστεί να είναι ένας συνηθισμένος χαρακτήρας έτσι ώστε να μην χρησιμοποιείται για να δείξει την έναρξη ενός σχολίου και μπορεί να χρησιμοποιηθεί αντ 'αυτού ως χειριστής διαίρεσης.

  • Ο χαρακτήρας μείον (-) έχει οριστεί να είναι ένας συνηθισμένος χαρακτήρας, έτσι ώστε η συμβολοσειρά "3-3" να χωρίζεται σε τρία διακριτικά - "3", "-" και "3" - και όχι μόνο "3" και "-3." (Θυμηθείτε, η ανάλυση αριθμών έχει οριστεί ως "ενεργοποιημένη" από προεπιλογή.)

Μόλις ρυθμιστεί το tokenizer, ο αναλυτής εντολών εκτελείται σε έναν άπειρο βρόχο (έως ότου αναγνωρίσει την εντολή "quit" στο σημείο που βγαίνει). Αυτό φαίνεται παρακάτω.

 8 ενώ (αληθινό) {9 Έκφραση έκφρασης; 10 int c = StreamTokenizer.TT_EOL; 11 συμβολοσειρά varName = null; 12 13 System.out.println ("Εισαγάγετε μια έκφραση ..."); 14 δοκιμάστε το {15 ενώ (true) {16 c = st.nextToken (); 17 εάν (c == StreamTokenizer.TT_EOF) {18 System.exit (1); 19} αλλιώς εάν (c == StreamTokenizer.TT_EOL) {20 συνεχίστε; 21} αλλιώς εάν (c == StreamTokenizer.TT_WORD) {22 if (st.sval.compareTo ("dump") == 0) {23 dumpVariables (μεταβλητές); 24 συνεχίστε? 25} αλλιώς εάν (st.sval.compareTo ("clear") == 0) {26 μεταβλητές = νέο Hashtable (); 27 συνεχίστε? 28} αλλιώς εάν (st.sval.compareTo ("quit") == 0) {29 System.exit (0); 30} αλλιώς εάν (st.sval.compareTo ("exit") == 0) {31 System.exit (0); 32} αλλιώς εάν (st.sval.compareTo ("help") == 0) {33 help (); 34 συνέχεια 35} 36 varName = st.sval; 37 c = st.nextToken (); 38} 39 διάλειμμα 40} 41 if (c! = '=') {42 ρίξτε νέο SyntaxError ("λείπει αρχικό '=' σημάδι."); 43} 

Όπως μπορείτε να δείτε στη γραμμή 16, το πρώτο διακριτικό καλείται με επίκληση επόμενο κουπόνι στο StreamTokenizer αντικείμενο. Αυτό επιστρέφει μια τιμή που δείχνει το είδος του διακριτικού που σαρώθηκε. Η τιμή επιστροφής είτε θα είναι μία από τις καθορισμένες σταθερές στο StreamTokenizer κλάση ή θα είναι μια τιμή χαρακτήρα. Τα διακριτικά "μετα" (αυτά που δεν είναι απλώς τιμές χαρακτήρων) ορίζονται ως εξής:

  • TT_EOF - Αυτό δείχνει ότι βρίσκεστε στο τέλος της ροής εισόδου. Διαφορετικός StringTokenizer, δεν υπάρχει hasMoreTokens μέθοδος.

  • TT_EOL - Αυτό σας λέει ότι το αντικείμενο μόλις πέρασε μια ακολουθία στο τέλος της γραμμής.

  • TT_NUMBER - Αυτός ο τύπος διακριτικού ενημερώνει τον κωδικό ανάλυσης ότι έχει εμφανιστεί ένας αριθμός στην είσοδο.

  • TT_WORD - Αυτός ο τύπος διακριτικού υποδηλώνει ότι σαρώθηκε ολόκληρη η λέξη.

Όταν το αποτέλεσμα δεν είναι μία από τις παραπάνω σταθερές, είναι είτε η τιμή χαρακτήρων που αντιπροσωπεύει έναν χαρακτήρα στο "συνηθισμένο" εύρος χαρακτήρων που σαρώθηκε είτε ένας από τους χαρακτήρες προσφοράς που έχετε ορίσει. (Στην περίπτωσή μου, δεν έχει οριστεί χαρακτήρας προσφοράς.) Όταν το αποτέλεσμα είναι ένας από τους χαρακτήρες προσφοράς, η αναφερόμενη συμβολοσειρά μπορεί να βρεθεί στη μεταβλητή παρουσίας συμβολοσειράς σβάλ απο StreamTokenizer αντικείμενο.

Ο κωδικός στις γραμμές 17 έως 20 ασχολείται με ενδείξεις στο τέλος της γραμμής και στο τέλος του αρχείου, ενώ στη γραμμή 21 λαμβάνεται η ρήτρα if εάν επιστραφεί ένα διακριτικό λέξης. Σε αυτό το απλό παράδειγμα, η λέξη είναι είτε μια εντολή είτε ένα όνομα μεταβλητής. Οι γραμμές 22 έως 35 αντιμετωπίζουν τις τέσσερις πιθανές εντολές. Εάν επιτευχθεί η γραμμή 36, τότε πρέπει να είναι ένα όνομα μεταβλητής. Κατά συνέπεια, το πρόγραμμα διατηρεί ένα αντίγραφο του ονόματος της μεταβλητής και παίρνει το επόμενο διακριτικό, το οποίο πρέπει να είναι ίσο.

Εάν στη γραμμή 41 το διακριτικό δεν ήταν ίσο, ο απλός αναλυτής μας ανιχνεύει κατάσταση σφάλματος και ρίχνει μια εξαίρεση για να το επισημάνει. Δημιούργησα δύο γενικές εξαιρέσεις, Συντακτικό λάθος και Σφάλμα Exec, για τη διάκριση σφαλμάτων ανάλυσης χρόνου από σφάλματα χρόνου εκτέλεσης. ο κύριος Η μέθοδος συνεχίζεται με τη γραμμή 44 παρακάτω.

44 res = ParseExpression.expression (st); 45} catch (SyntaxError se) {46 res = null; 47 varName = null; 48 System.out.println ("\ n Εντοπίστηκε σφάλμα σύνταξης! -" + se.getMsg ()); 49 ενώ (c! = StreamTokenizer.TT_EOL) 50 c = st.nextToken (); 51 συνεχίστε. 52} 

Στη γραμμή 44, η έκφραση στα δεξιά του ίσου σημείου αναλύεται με τον αναλυτή έκφρασης που ορίζεται στο ParseExpression τάξη. Σημειώστε ότι οι γραμμές 14 έως 44 είναι τυλιγμένες σε ένα μπλοκ δοκιμής / παγίδευσης που παγιδεύει σφάλματα σύνταξης και τα αντιμετωπίζει. Όταν εντοπιστεί ένα σφάλμα, η ενέργεια ανάκτησης του αναλυτή είναι να καταναλώσει όλα τα διακριτικά έως και το επόμενο διακριτικό τέλους της γραμμής. Αυτό φαίνεται στις γραμμές 49 και 50 παραπάνω.

Σε αυτό το σημείο, εάν δεν έγινε εξαίρεση, η εφαρμογή ανέλυσε με επιτυχία μια δήλωση. Ο τελικός έλεγχος είναι να δούμε ότι το επόμενο διακριτικό είναι το τέλος της γραμμής. Εάν δεν είναι, ένα σφάλμα δεν εντοπίστηκε. Το πιο συνηθισμένο σφάλμα θα είναι οι αναντιστοιχίες παρενθέσεων. Αυτός ο έλεγχος εμφανίζεται στις γραμμές 53 έως 60 του παρακάτω κώδικα.

53 c = st.nextToken (); 54 if (c! = StreamTokenizer.TT_EOL) {55 if (c == ')') 56 System.out.println ("\ n Εντοπίστηκε σφάλμα σύνταξης! - Σε πολλά κλειστά γονέα."); 57 ακόμη 58 System.out.println ("\ nBogus token on input -" + c); 59 ενώ (c! = StreamTokenizer.TT_EOL) 60 c = st.nextToken (); 61} αλλιώς { 

Όταν το επόμενο διακριτικό είναι το τέλος της γραμμής, το πρόγραμμα εκτελεί τις γραμμές 62 έως 69 (φαίνεται παρακάτω). Αυτή η ενότητα της μεθόδου αξιολογεί την αναλυμένη έκφραση. Εάν το όνομα της μεταβλητής ορίστηκε στη γραμμή 36, το αποτέλεσμα αποθηκεύεται στον πίνακα συμβόλων. Και στις δύο περιπτώσεις, εάν δεν υπάρξει εξαίρεση, η έκφραση και η τιμή της εκτυπώνονται στη ροή System.out έτσι ώστε να μπορείτε να δείτε τι αποκωδικοποιήθηκε ο αναλυτής.

62 δοκιμάστε το {63 Double z; 64 System.out.println ("Αναλυμένη έκφραση:" + res.unparse ()); 65 z = νέο Double (res.value (μεταβλητές)); 66 System.out.println ("Η τιμή είναι:" + z); 67 if (varName! = Null) {68 variables.put (varName, z); 69 System.out.println ("Ανατέθηκε σε:" + varName); 70} 71} catch (ExecError ee) {72 System.out.println ("Σφάλμα εκτέλεσης," + ee.getMsg () + "!"); 73} 74} 75} 76} 

Στο Παράδειγμα ST τάξη, το StreamTokenizer χρησιμοποιείται από έναν αναλυτή επεξεργαστή εντολών. Αυτός ο τύπος parser συνήθως χρησιμοποιείται σε ένα πρόγραμμα κελύφους ή σε οποιαδήποτε κατάσταση στην οποία ο χρήστης εκδίδει αλληλεπιδραστικές εντολές. Ο δεύτερος αναλυτής ενσωματώνεται στο ParseExpression τάξη. (Δείτε την ενότητα Πόροι για την πλήρη πηγή.) Αυτή η κλάση αναλύει τις εκφράσεις της αριθμομηχανής και καλείται στη γραμμή 44 παραπάνω. Εδώ είναι αυτό StreamTokenizer αντιμετωπίζει την πιο σκληρή πρόκληση.

Δημιουργία αναλυτή έκφρασης

Η γραμματική για τις εκφράσεις της αριθμομηχανής καθορίζει μια αλγεβρική σύνταξη της φόρμας "[item] operator [item]." Αυτός ο τύπος γραμματικής εμφανίζεται ξανά και ξανά και ονομάζεται χειριστής γραμματική. Μια βολική σημείωση για μια γραμματική χειριστή είναι:

id ("OPERATOR" id) * 

Ο παραπάνω κωδικός θα διαβαστεί "Ένα τερματικό ταυτότητας ακολουθούμενο από μηδέν ή περισσότερες εμφανίσεις μιας πλειάδας χειριστή-ταυτότητας." ο StreamTokenizer Η τάξη θα ήταν αρκετά ιδανική για την ανάλυση τέτοιων ροών, επειδή ο σχεδιασμός διασπά φυσικά τη ροή εισόδου λέξη, αριθμός, και συνηθισμένος χαρακτήρας μάρκες. Όπως θα σας δείξω, αυτό ισχύει έως ένα σημείο.

ο ParseExpression Το μάθημα είναι ένας απλός, αναδρομικός-καταδυτικός αναλυτής για εκφράσεις, απευθείας από μια προπτυχιακή τάξη μεταγλωττιστή-σχεδιασμού. ο Εκφραση μέθοδος σε αυτήν την τάξη ορίζεται ως εξής:

 1 έκφραση στατικής έκφρασης (StreamTokenizer st) ρίχνει το SyntaxError {2 Expression αποτέλεσμα. 3 boolean done = false; 4 5 αποτέλεσμα = άθροισμα (st); 6 ενώ (! Ολοκληρώθηκε) {7 δοκιμάστε {8 διακόπτης (st.nextToken ()) 9 case '&': 10 result = new Expression (OP_AND, result, sum (st)); 11 διάλειμμα 12 case '23} catch (IOException χρόνο) {24 ρίξτε νέο SyntaxError ("Πήρα εξαίρεση I / O"); 25} 26} 27 αποτέλεσμα επιστροφής. 28}