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

Δημιουργήστε τις δικές σας γλώσσες με το JavaCC

Αναρωτιέστε ποτέ πώς λειτουργεί ο μεταγλωττιστής Java; Πρέπει να γράψετε αναλυτές για έγγραφα σήμανσης που δεν εγγράφονται σε τυπικές μορφές όπως HTML ή XML; Ή θέλετε να εφαρμόσετε τη δική σας μικρή γλώσσα προγραμματισμού μόνο για το καλό της; JavaCC σας επιτρέπει να κάνετε όλα αυτά στην Java. Επομένως, αν σας ενδιαφέρει απλώς να μάθετε περισσότερα σχετικά με το πώς λειτουργούν οι μεταγλωττιστές και οι διερμηνείς ή έχετε συγκεκριμένες φιλοδοξίες να δημιουργήσετε τον διάδοχο της γλώσσας προγραμματισμού Java, παρακαλώ εγγραφείτε μαζί μου στην αναζήτηση αυτού του μήνα JavaCC, τονίζεται από την κατασκευή μιας εύχρηστης αριθμομηχανής γραμμής εντολών.

Βασικές αρχές κατασκευής μεταγλωττιστή

Οι γλώσσες προγραμματισμού χωρίζονται συχνά, κάπως τεχνητά, σε μεταγλωττισμένες και ερμηνευμένες γλώσσες, αν και τα όρια έχουν γίνει θολά. Ως εκ τούτου, μην ανησυχείτε για αυτό. Οι έννοιες που συζητούνται εδώ ισχύουν εξίσου καλά για τις μεταγλωττισμένες και τις ερμηνευμένες γλώσσες. Θα χρησιμοποιήσουμε τη λέξη μεταγλωττιστής παρακάτω, αλλά για το πεδίο εφαρμογής αυτού του άρθρου, αυτό θα περιλαμβάνει την έννοια του διερμηνέας.

Οι μεταγλωττιστές πρέπει να εκτελούν τρεις σημαντικές εργασίες όταν παρουσιάζονται με ένα κείμενο προγράμματος (πηγαίος κώδικας):

  1. Λεξική ανάλυση
  2. Συντακτική ανάλυση
  3. Δημιουργία ή εκτέλεση κώδικα

Το μεγαλύτερο μέρος της εργασίας του μεταγλωττιστή επικεντρώνεται στα βήματα 1 και 2, τα οποία περιλαμβάνουν την κατανόηση του πηγαίου κώδικα του προγράμματος και τη διασφάλιση της συντακτικής του ορθότητας. Καλούμε αυτή τη διαδικασία τεχνολογία, Ποιο είναι το αναλυτήςευθύνη.

Λεξική ανάλυση (lexing)

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

Συντακτική ανάλυση (ανάλυση)

Κατά τη διάρκεια της συντακτικής ανάλυσης, ένας αναλυτής εξάγει νόημα από τον πηγαίο κώδικα του προγράμματος διασφαλίζοντας τη συντακτική ορθότητα του προγράμματος και δημιουργώντας μια εσωτερική αναπαράσταση του προγράμματος.

Η θεωρία της γλώσσας υπολογιστών αναφέρεται προγράμματα,γραμματική, και Γλώσσες. Υπό αυτήν την έννοια, ένα πρόγραμμα είναι μια ακολουθία διακριτικών. Το κυριολεκτικό είναι ένα βασικό στοιχείο γλώσσας υπολογιστή που δεν μπορεί να μειωθεί περαιτέρω. Μια γραμματική ορίζει κανόνες για τη δημιουργία συντακτικών σωστών προγραμμάτων. Μόνο τα προγράμματα που παίζουν με τους κανόνες που ορίζονται στη γραμματική είναι σωστά. Η γλώσσα είναι απλώς το σύνολο όλων των προγραμμάτων που ικανοποιούν όλους τους κανόνες γραμματικής σας.

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

Οι κανόνες γραμματικής μιας γλώσσας υπολογιστή μπορούν να καθοριστούν με σαφήνεια και στο σύνολό τους με τη σημείωση EBNF (Extended Backus-Naur-Form) (για περισσότερα σχετικά με το EBNF, βλ. Πόρους) Το EBNF ορίζει γραμματικές από την άποψη των κανόνων παραγωγής. Ένας κανόνας παραγωγής δηλώνει ότι ένα στοιχείο γραμματικής - είτε κυριολεκτικά είτε σύνθετα στοιχεία - μπορεί να αποτελείται από άλλα στοιχεία γραμματικής. Τα λογικά στοιχεία, τα οποία είναι αμετάκλητα, είναι λέξεις-κλειδιά ή θραύσματα στατικού στατικού προγράμματος, όπως σύμβολα στίξης. Τα συντεθέντα στοιχεία παράγονται με την εφαρμογή κανόνων παραγωγής. Οι κανόνες παραγωγής έχουν την ακόλουθη γενική μορφή:

GRAMMAR_ELEMENT: = λίστα στοιχείων γραμματικής | εναλλακτική λίστα στοιχείων γραμματικής 

Για παράδειγμα, ας δούμε τους κανόνες γραμματικής για μια μικρή γλώσσα που περιγράφει βασικές αριθμητικές εκφράσεις:

expr: = αριθμός | expr '+' expr | expr '-' expr | expr '*' expr | expr '/' expr | "(" expr ")" | - αριθμός expr: = ψηφίο + ('.' ψηφίο +); ψηφίο: = '0' | «1» | «2» | «3» | «4» | «5» | «6» | «7» | «8» | «9» 

Τρεις κανόνες παραγωγής ορίζουν τα στοιχεία γραμματικής:

  • π.χ.
  • αριθμός
  • ψηφίο

Η γλώσσα που ορίζεται από αυτήν τη γραμματική μας επιτρέπει να καθορίσουμε αριθμητικές εκφράσεις. Ενα π.χ. είναι είτε ένας αριθμός είτε ένας από τους τέσσερις τελεστές infix που εφαρμόζονται σε δύο π.χ.s, ένα π.χ. σε παρένθεση ή αρνητικό π.χ.. ΕΝΑ αριθμός είναι ένας αριθμός κινητής υποδιαστολής με προαιρετικό δεκαδικό κλάσμα. Ορίζουμε ένα ψηφίο να είναι ένα από τα γνωστά δεκαδικά ψηφία.

Δημιουργία ή εκτέλεση κώδικα

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

JavaCC

JavaCC, διατίθεται δωρεάν, είναι μια γεννήτρια ανάλυσης. Παρέχει μια επέκταση γλώσσας Java για τον καθορισμό της γραμματικής μιας γλώσσας προγραμματισμού. JavaCC αναπτύχθηκε αρχικά από την Sun Microsystems, αλλά τώρα συντηρείται από τα MetaMata. Όπως οποιοδήποτε αξιοπρεπές εργαλείο προγραμματισμού, JavaCC χρησιμοποιήθηκε στην πραγματικότητα για να καθορίσει τη γραμματική του JavaCC μορφή εισαγωγής.

Εξάλλου, JavaCC μας επιτρέπει να ορίσουμε γραμματικές με τρόπο παρόμοιο με το EBNF, καθιστώντας εύκολη τη μετάφραση γραμματικών EBNF στο JavaCC μορφή. Περαιτέρω, JavaCC είναι η πιο δημοφιλής γεννήτρια parser για Java, με πλήθος προκαθορισμένων JavaCC Γραμματικές διαθέσιμες για χρήση ως σημείο εκκίνησης.

Ανάπτυξη ενός απλού υπολογιστή

Τώρα επανεξετάζουμε τη μικρή αριθμητική γλώσσα μας για να δημιουργήσουμε μια απλή αριθμομηχανή γραμμής εντολών χρησιμοποιώντας Java JavaCC. Πρώτον, πρέπει να μεταφράσουμε τη γραμματική του EBNF JavaCC μορφοποιήστε και αποθηκεύστε το στο αρχείο Arithmetic.jj:

επιλογές {LOOKAHEAD = 2; } PARSER_BEGIN (αριθμητική) δημόσια τάξη αριθμητική {} PARSER_END (αριθμητική) SKIP: "\ t" TOKEN: double expr (): {} term () ("+" expr () double term (): {} "/" term ()) * διπλό unary (): {} "-" element () double element (): {} "(" expr () ")" 

Ο παραπάνω κώδικας θα σας δώσει μια ιδέα για τον τρόπο καθορισμού μιας γραμματικής JavaCC. ο επιλογές Το τμήμα στην κορυφή καθορίζει ένα σύνολο επιλογών για αυτήν τη γραμματική. Καθορίζουμε ένα lookahead 2. Έλεγχος πρόσθετων επιλογών JavaCCΛειτουργίες εντοπισμού σφαλμάτων και πολλά άλλα. Αυτές οι επιλογές μπορούν εναλλακτικά να καθοριστούν στο JavaCC γραμμή εντολών.

ο PARSER_BEGIN ο όρος καθορίζει ότι ακολουθεί ο ορισμός της κατηγορίας parser. JavaCC δημιουργεί μία μεμονωμένη κλάση Java για κάθε πρόγραμμα ανάλυσης. Καλούμε την κατηγορία αναλυτών Αριθμητική. Προς το παρόν, απαιτούμε μόνο έναν κενό ορισμό κλάσης. JavaCC θα προσθέσει αργότερα τις σχετικές δηλώσεις ανάλυσης. Τελειώνουμε τον ορισμό της τάξης με το PARSER_END ρήτρα.

ο ΠΑΡΑΛΕΙΠΩ Η ενότητα προσδιορίζει τους χαρακτήρες που θέλουμε να παραλείψουμε. Στην περίπτωσή μας, αυτοί είναι οι λευκοί χαρακτήρες. Στη συνέχεια, ορίζουμε τα διακριτικά της γλώσσας μας στο ΕΝΔΕΙΞΗ Ενότητα. Ορίζουμε αριθμούς και ψηφία ως διακριτικά. Σημειώστε ότι JavaCC κάνει διάκριση μεταξύ ορισμών για διακριτικά και ορισμών για άλλους κανόνες παραγωγής, που διαφέρει από το EBNF. ο ΠΑΡΑΛΕΙΠΩ και ΕΝΔΕΙΞΗ Οι ενότητες καθορίζουν τη λεξική ανάλυση αυτής της γραμματικής.

Στη συνέχεια, ορίζουμε τον κανόνα παραγωγής για π.χ., το στοιχείο γραμματικής ανώτατου επιπέδου. Παρατηρήστε πώς ο ορισμός αυτός διαφέρει σημαντικά από τον ορισμό του π.χ. στο EBNF. Τι συμβαίνει? Λοιπόν, αποδεικνύεται ότι ο παραπάνω ορισμός EBNF είναι διφορούμενος, καθώς επιτρέπει πολλαπλές αναπαραστάσεις του ίδιου προγράμματος. Για παράδειγμα, ας εξετάσουμε την έκφραση 1+2*3. Μπορούμε να ταιριάξουμε 1+2 σε ένα π.χ. ενδοτικότητα expr * 3, όπως στο Σχήμα 1.

Ή, εναλλακτικά, θα μπορούσαμε πρώτα να ταιριάξουμε 2*3 σε ένα π.χ. έχοντας ως αποτέλεσμα 1 + expr, όπως φαίνεται στο σχήμα 2.

Με JavaCC, πρέπει να καθορίσουμε σαφώς τους κανόνες γραμματικής. Ως αποτέλεσμα, διαγράφουμε τον ορισμό του π.χ. σε τρεις κανόνες παραγωγής, ορίζοντας τα γραμματικά στοιχεία π.χ., όρος, unary, και στοιχείο. Τώρα, η έκφραση 1+2*3 αναλύεται όπως φαίνεται στο σχήμα 3.

Από τη γραμμή εντολών μπορούμε να τρέξουμε JavaCC για να ελέγξετε τη γραμματική μας:

javacc Arithmetic.jj Java Compiler Compiler Έκδοση 1.1 (Parser Generator) Πνευματικά δικαιώματα (c) 1996-1999 Sun Microsystems, Inc. Πνευματικά δικαιώματα (c) 1997-1999 Metamata, Inc. (τύπος "javacc" χωρίς επιχειρήματα για βοήθεια) Ανάγνωση από αρχείο Arithmetic.jj. . . Προειδοποίηση: Δεν πραγματοποιείται έλεγχος επάρκειας Lookahead, καθώς η επιλογή LOOKAHEAD είναι μεγαλύτερη από 1. Ορίστε την επιλογή FORCE_LA_CHECK σε true για να επιβάλλετε τον αναγκαστικό έλεγχο. Δημιουργήθηκε πρόγραμμα ανάλυσης με 0 σφάλματα και 1 προειδοποιήσεις. 

Τα παρακάτω ελέγχουν τον ορισμό της γραμματικής μας για προβλήματα και δημιουργούν ένα σύνολο αρχείων προέλευσης Java:

TokenMgrError.java ParseException.java Token.java ASCII_CharStream.java Arithmetic.java ArithmeticConstants.java ArithmeticTokenManager.java 

Μαζί αυτά τα αρχεία εφαρμόζουν το πρόγραμμα ανάλυσης σε Java. Μπορείτε να καλέσετε αυτόν τον αναλυτή, δημιουργώντας μια παρουσία του Αριθμητική τάξη:

δημόσια τάξη Arithmetic εφαρμόζει ArithmeticConstants {public Arithmetic (java.io.InputStream stream) {...} public Arithmetic (java.io.Reader stream) {...} δημόσια αριθμητική (ArithmeticTokenManager tm) {...} στατικό τελικό κοινό double expr () ρίχνει ParseException {...} στατικό τελικό δημόσιο διπλό όρος () ρίχνει ParseException {...} στατικό τελικό δημόσιο διπλό unary () ρίχνει ParseException {...} στατικό τελικό δημόσιο διπλό στοιχείο () ρίχνει ParseException {. ..} στατικό δημόσιο κενό ReInit (java.io.InputStream stream) {...} στατικό δημόσιο κενό ReInit (java.io.Reader stream) {...} δημόσιο κενό ReInit (ArithmeticTokenManager tm) {...} στατικό final public Token getNextToken () {...} static final public Token getToken (int index) {...} στατικό τελικό δημόσιο ParseException createParseException () {...} στατικό τελικό δημόσιο άκυρο activ_tracing () {...} στατικό final public void disable_tracing () {...}} 

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

Αριθμητικός αναλυτής = νέος αριθμητικός (System.in); parser.expr (); 

Ωστόσο, τίποτα δεν συμβαίνει ακόμα επειδή στο Arithmetic.jj ορίσαμε μόνο τους κανόνες γραμματικής. Δεν έχουμε προσθέσει ακόμη τον απαραίτητο κωδικό για την εκτέλεση των υπολογισμών. Για να το κάνουμε αυτό, προσθέτουμε τις κατάλληλες ενέργειες στους κανόνες γραμματικής. Calcualtor.jj περιέχει την πλήρη αριθμομηχανή, συμπεριλαμβανομένων των ενεργειών:

επιλογές {LOOKAHEAD = 2; } PARSER_BEGIN (Αριθμομηχανή) υπολογιστής δημόσιας τάξης {public static void main (String args []) ρίχνει το ParseException {Calculator parser = new Calculator (System.in); ενώ (true) {parser.parseOneLine (); }}} PARSER_END (Αριθμομηχανή) ΠΑΡΑΚΟΛΟΥΘΗΣΗ: "\ t" TOKEN: void parseOneLine (): {double a; } {a = expr () {System.out.println (a); } | | {System.exit (-1); }} διπλό expr (): {double a; διπλό β; } {a = όρος () ("+" b = expr () {a + = b;} | "-" b = expr () {a - = b;}) * {επιστροφή a; }} διπλός όρος (): {double a; διπλό β; } {a = unary () ("*" b = term () {a * = b;} | "/" b = term () {a / = b;}) * {επιστροφή a; }} διπλό unary (): {double a; } {"-" a = element () {return -a; } | a = στοιχείο () {επιστροφή a; }} διπλό στοιχείο (): {Token t; διπλό α? } {t = {return Double.parseDouble (t.toString ()); } | "(" a = expr () ")" {return a; }} 

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

Έχουμε αλλάξει τον τύπο επιστροφής των αρχικών στοιχείων γραμματικής για επιστροφή διπλό. Πραγματοποιούμε κατάλληλους υπολογισμούς ακριβώς όπου τους αναλύουμε και περνάμε τα αποτελέσματα του υπολογισμού στο δέντρο κλήσεων. Έχουμε επίσης μεταμορφώσει τους ορισμούς του στοιχείου γραμματικής για να αποθηκεύσουμε τα αποτελέσματά τους σε τοπικές μεταβλητές. Για παράδειγμα, a = στοιχείο () αναλύει ένα στοιχείο και αποθηκεύει το αποτέλεσμα στη μεταβλητή ένα. Αυτό μας επιτρέπει να χρησιμοποιήσουμε τα αποτελέσματα των αναλυμένων στοιχείων στον κώδικα των ενεργειών στη δεξιά πλευρά. Οι ενέργειες είναι μπλοκ κώδικα Java που εκτελούνται όταν ο σχετικός κανόνας γραμματικής εντοπίσει αντιστοίχιση στη ροή εισόδου.

Σημειώστε πόσο λίγος κώδικας Java προσθέσαμε για να κάνουμε την αριθμομηχανή πλήρως λειτουργική. Επιπλέον, η προσθήκη πρόσθετης λειτουργικότητας, όπως ενσωματωμένες συναρτήσεις ή ακόμη και μεταβλητές είναι εύκολη.

$config[zx-auto] not found$config[zx-overlay] not found