Creative Commons Attribution-ShareAlike 4.0 International License

Εξαγωγή 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):

lexeme

Το κείμενο που προκάλεσε το σφάλμα.

lineno

Ο αριθμός γραμμης του σημείου που προκάλεσε το σφάλμα.

charpos

Ο αριθμός στήλης (χαρακτήρα) του σημείου που προκάλεσε το σφάλμα.

message

Γενικό μήνυμα περιγραφής του σφάλματος.

Παράδειγμα: 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.

Ασκήσεις

  1. Κατασκευάστε με τη βοήθεια του 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, αντίστοιχα.

  1. Προσπαθήστε να προσθέσετε στον προηγούμενο λεκτικό αναλυτή τη δυνατότητα αναγνώρισης σχολίων μονής γραμμής στο στυλ της C: ξεκινώντας από \\ και έως το τέλος της γραμμής (μέχρι να βρείτε newline). Τα σχόλια θα πρέπει να αγνοούνται.