Εξαγωγή tokens¶
Το στάδιο της λεκτικής ανάλυσης σε έναν μεταγλωττιστή δέχεται ως είσοδο τον πηγαίο κώδικα ενός προγράμματος και παράγει σύμβολα (tokens) που αντιπροσωπεύουν keywords, ονόματα μεταβλητών, αριθμητικές σταθερές, τελεστές κ.ο.κ. Στο στάδιο αυτό αναγνωρίζονται επίσης τα κενά και τα σχόλια, τα οποία συνήθως απορρίπτονται.
Μπορούμε να κατασκευάσουμε έναν λεκτικό αναλυτή με τη βοήθεια των κανονικών εκφράσεων συνενώνοντας με την εναλλαγή (|) μια σειρά από επιθυμητά patterns προς αναγνώριση:
(pattern1)|(pattern2)|(pattern3)|...|(patternN)
Ένα πλήρες παράδειγμα για το πώς μπορεί να γίνει το προηγούμενο, μαζί με τον τρόπο χρήσης των groups για να ξέρουμε ποιο pattern ταίριαξε κάθε φορά, θα βρείτε στην τεκμηρίωση της βιβλιοθήκης re
της Python: “Writing a Tokenizer” .
Η κλάση Tokenizer¶
Στις βιβλιοθήκες του εργαστηρίου θα βρείτε την κλάση Tokenizer
που υλοποιεί έναν λεκτικό αναλυτή για την εξαγωγή tokens μέσω κανονικών εκφράσεων της Python, όπως περιγράφηκε προηγουμένως.
Γίνεται import (μαζί το σφάλμα TokenizerError
και τις σταθερές TokenAction
) στο πρόγραμμά μας ως εξής:
from compilerlabs import Tokenizer, TokenizerError, TokenAction
Ακολουθούν η περιγραφή και παραδείγματα χρήσης του Tokenizer.
- class Tokenizer¶
Δημιουργεί ένα καινούργιο αντικείμενο τύπου Tokenizer.
t = Tokenizer()
- pattern(regex, token, keywords=None)¶
Προσθέτει στον λεκτικό αναλυτή τη δυνατότητα αναγνώρισης του pattern regex (ένα string κανονικής έκφρασης, όπως αυτά που δίνονται ως είσοδος στο
re.compile()
).Η τιμή του token επιστρέφεται όταν αναγνωριστεί το συγκεκριμένο pattern. Ως token μπορεί επίσης να δοθεί μία από τις σταθερές
TokenAction
, προσδιορίζοντας ειδικό χειρισμό κατά την αναγνώριση του pattern.Εάν στα στοιχεία που αναγνωρίζονται από το pattern υπάρχουν strings για τα οποία απαιτείται ειδικός χειρισμός (π.χ. τα keywords μιας γλώσσας προγραμματισμού), αυτά μπορούν να δοθούν μέσω της προαιρετικής παραμέτρου keywords.
Η παράμετρος keywords μπορεί να είναι μια ακολουθία (π.χ. λίστα ή tuple) ή ένα set. Στην περίπτωση αυτή, όταν αναγνωριστεί κείμενο που περιέχεται στο keywords, αυτό επιστρέφεται και ως token και ως lexeme:
>>> t = Tokenizer() >>> t.pattern('[a-zA-z]+','word',keywords=('if', 'else')) >>> t.pattern(r'\s+',TokenAction.IGNORE) >>> for s in t.scan('abc if ifa else elser'): ... print(s.token,s.lexeme) ... word abc if if word ifa else else word elser None
Εάν το keywords είναι ένα λεξικό (dict) τότε ως token επιστρέφεται η τιμή του λεξικού που αντιστοιχεί στο κλειδί που αναγνωρίστηκε:
>>> from compilerlabs import Tokenizer,TokenizerError,TokenAction >>> t = Tokenizer() >>> t.pattern('[a-zA-z]+','word',keywords={'if':'KW_IF', 'else':'KW_ELSE'}) >>> t.pattern(r'\s+',TokenAction.IGNORE) >>> for s in t.scan('abc if ifa else elser'): ... print(s.token,s.lexeme) ... word abc KW_IF if word ifa KW_ELSE else word elser None
Η υλοποίηση του λεκτικού αναλυτή χρησιμοποιεί την εναλλαγή (|) για να συνδέσει πολλαπλά patterns που αναγνωρίζουν tokens σε μια μεγάλη κανονική έκφραση. Συνεπώς, η σειρά που δηλώνονται τα patterns προσδιορίζει και την προτεραιότητά τους κατά την αναγνώριση.
- scan(text, flags=0, eot=True)¶
Επιστρέφει μια ακολουθία συμβόλων αναλύοντας λεκτικά το κείμενο text. Κάθε σύμβολο είναι μια πλειάδα (namedtuple) της μορφής (token, lexeme, lineno, charpos) όπου:
Το token αντιστοιχεί στο pattern που αναγνωρίστηκε.
Το lexeme περιέχει το κείμενο που αναγνωρίστηκε.
Τα lineno και charpos δίνουν τη θέση στο συνολικό κείμενο του κειμένου που αναγνωρίστηκε.
Με την παράμετρο flags μπορούν προαιρετικά να δηλωθούν σταθερές που δέχεται η
re.compile()
(π.χ. re.IGNORECASE). Εάν η προαιρετική παράμετρος eot είναι αληθής, ο λεκτικός αναλυτής επιστρέφει ένα token ίσο με None όταν τελειώσει το κείμενο εισόδου.
Παράδειγμα χρήσης¶
from compilerlabs import Tokenizer,TokenAction,TokenizerError
t = Tokenizer()
t.pattern('[a-zA-Z]+','word',('print','int','def'))
t.pattern('[0-9]+','number')
t.pattern('[-+*/=]',TokenAction.TEXT)
t.pattern(r'\s+',TokenAction.IGNORE)
t.pattern('.',TokenAction.ERROR)
text = """hi 123-7
666
b + m 5 int/ 92
so78*6"""
try:
for s in t.scan(text,eot=False):
print(s)
except TokenizerError as e:
print(e)
Μπορείτε να κατεβάσετε το αρχείο του παραδείγματος εδώ
.
Σταθερές TokenAction¶
- class TokenAction¶
Επιλογές ειδικού χειρισμού κατά την αναγνώριση ενός pattern. Περιλαμβάνονται οι εξής σταθερές:
- TokenAction.IGNORE¶
Το token που αναγνωρίστηκε δεν επιστρέφεται και ο λεκτικός αναλυτής συνεχίζει στο επόμενο.
- TokenAction.TEXT¶
Επιστρέφεται ως token το κείμενο (lexeme) που αναγνωρίστηκε.
- TokenAction.ERROR¶
Δημιουργείται σφάλμα
TokenizerError
.
Σφάλμα λεκτικού αναλυτή¶
- exception TokenizerError¶
Δημιουργείται όταν αναγνωριστεί pattern του οποίου το token έχει προσδιοριστεί με την ειδική επιλογή
TokenAction.ERROR
. Θα πρέπει να σημειωθεί ότι κείμενο που δεν αναγνωρίζεται από κανένα pattern απλά αγνοείται.Το σφάλμα περιέχει τα εξής χαρακτηριστικά (attributes):
Παράδειγμα: Postfix calculator¶
Ο λεκτικός αναλυτής επιστρέφει μια σειρά από tokens χωρίς να εκτελεί οποιουδήποτε τύπου συντακτική ανάλυση. Έτσι, δεν είναι εύκολο να φτιαχτεί χρήσιμη εφαρμογή μόνο με αυτόν. Υπάρχει όμως ένας τρόπος γραφής αριθμητικών εκφράσεων (postfix notation), ο οποίος επιτρέπει τον υπολογισμό τους χωρίς συντακτική ανάλυση. Το παράδειγμα αυτό θα δούμε στη συνέχεια.
Στη μορφή postfix ο τελεστής γράφεται αμέσως μετά τα δεδομένα εισόδου. Π.χ. η έκφραση 3+5
γράφεται ως:
3 5 +
ενώ η έκφραση 8*(3+2)
γράφεται ως:
8 3 2 + *
Εάν κάθε τελεστής έχει προκαθορισμένο αριθμό εισόδων (π.χ. ξέρουμε ότι το +
δέχεται πάντα δύο εισόδους), τότε μπορείτε να υπολογίσετε την αριθμητική έκφραση χωρίς συντακτική ανάλυση, με τη βοήθεια μιας στοίβας (stack) και σύμφωνα με τον εξής αλγόριθμο:
Για κάθε token εισόδου:
Εάν το token είναι αριθμός:
Βάλε (push) τον αριθμό στη στοίβα
Αλλιώς, αν είναι τελεστής:
Πάρε (pop) από τη στοίβα όσα στοιχεία
χρειάζεται ο τελεστής
Κάνε την πράξη
Βάλε (push) το αποτέλεσμα στη στοίβα
Όταν εξαντληθούν τα tokens εισόδου, στην κορυφή της στοίβας θα βρίσκεται το τελικό αποτέλεσμα.
Το επόμενο πρόγραμμα Python υλοποιεί τον αλγόριθμο για υπολογισμό αριθμητικών εκφράσεων σε postfix μορφή. Οι υποστηριζόμενοι τελεστές ειναι οι +
, -
, *
, /
και η εντολή print
, η οποίη τυπώνει ό,τι βρίσκεται στην κορυφή της στοίβας εκείνη τη στιγμή (χωρίς να αλλάζει το περιεχόμενο της στοίβας).
from compilerlabs import Tokenizer,TokenAction,TokenizerError, \
Stack,StackError
t = Tokenizer()
t.pattern(r'[0-9]+(\.[0-9]+)?','NUMBER')
t.pattern('[-+*/]','OPERATOR')
t.pattern('print','COMMAND')
t.pattern(r'\s+',TokenAction.IGNORE)
t.pattern('.',TokenAction.ERROR)
# functions of operators
def add(stack):
b = stack.pop()
a = stack.pop()
stack.push(a+b)
def sub(stack):
b = stack.pop()
a = stack.pop()
stack.push(a-b)
def mult(stack):
b = stack.pop()
a = stack.pop()
stack.push(a*b)
def div(stack):
b = stack.pop()
a = stack.pop()
stack.push(a/b)
# functions of commands
def prn(stack):
a = stack.pop()
print(a)
stack.push(a) # put back item in stack
# dict of operator/command functions
fn = {'+':add, '-':sub, '*':mult, '/':div, 'print':prn }
text = """
1.1 10 7 - 6 2 / * + print
"""
stack = Stack()
try:
for symbol in t.scan(text):
token = symbol.token
lexeme = symbol.lexeme
if token=='NUMBER':
stack.push(float(lexeme)) # push into stack arithmetic value
elif token=='OPERATOR' or token=='COMMAND':
fn[lexeme](stack) # call operator's/command's function
except TokenizerError as e:
print(e)
except StackError:
print('Input error at line {symbol.lineno} char {symbol.charpos}: stack is empty')
Στο προηγούμενο παράδειγμα, χρησιμοποιούμε μια συνάρτηση ανά τελεστή και ένα dictionary για να επιλέγουμε συνάρτηση ανάλογα με τον τελεστή/εντολή αντί για μια σειρά από if..elif..elif..else
.
Το παράδειγμα υπολογίζει την έκφραση
1.1 10 7 - 6 2 / * + ?
υπολογίζει δηλαδή το 1.1+(10-7)*(6/2)
.
Μπορείτε να κατεβάσετε το αρχείο με τον κώδικα Python
του postfix calculator.
Ασκήσεις¶
Κατασκευάστε με τη βοήθεια του module
Tokenizer
λεκτικό αναλυτή ο οποίος θα αναγνωρίζει αριθμητικές σταθερές (literals) στις εξής μορφές:
ακέραιες σταθερές (σειρές από ψηφία 0-9 οποιουδήποτε μήκους)
κλασματικές (float) σταθερές στη μορφή
xxx.yyy
,.yyy
καιxxx.
(με οποιοδήποτε μήκος x και y)δεκαεξαδικές σταθερές (hex), στη μορφή
0x****
όπου το*
αντιστοιχεί σε ένα δεκαεξαδικό ψηφίο (0-9, a-f, A-F) (για οποιονδήποτε αριθμό ψηφίων)Ο λεκτικός αναλυτής θα πρέπει να επιστρέφει
INT_TOKEN
,FLOAT_TOKEN
καιHEX_TOKEN
, αντίστοιχα.
Προσπαθήστε να προσθέσετε στον προηγούμενο λεκτικό αναλυτή τη δυνατότητα αναγνώρισης σχολίων μονής γραμμής στο στυλ της C: ξεκινώντας από
\\
και έως το τέλος της γραμμής (μέχρι να βρείτε newline). Τα σχόλια θα πρέπει να αγνοούνται.