Σημειώσεις Μεταγλωττιστές

Download Report

Transcript Σημειώσεις Μεταγλωττιστές

Σηµειώσεις Μεταγλωττιστές
1. Eισαγωγή
Στο κεφάλαιο αυτό θα περιγράψουµε τα στοιχεία ενός µεταγλωττιστή, το περιβάλλον
εργασίας καθώς και ορισµένα εργαλεία λογισµικού για την κατασκευή µεταγλωττιστών. Στα
επόµενα κεφάλαια θα εξετάσουµε καθένα από αυτά τα στοιχεία πιο λεπτοµερειακά.
1.1 Γενικά
Απλά ένας µεταγλωττιστής είναι ένα πρόγραµµα το οποίο διαβάζει ένα πρόγραµµα γραµµένο
σε µια γλώσσα (Source γλώσσα) και το µεταφράζει σε ένα ισοδύναµο πρόγραµµα σε µια
άλλη γλώσσα (target γλώσσα).
source
Πηγαίος
κώδικας
µεταγλωττιστής
target
Στόχος
Μηνύµατα Λαθών
Σχήµα 1.1
H κατασκευή ενός µεταγλωττιστή είναι µια πολύπλοκη διαδικασία. Eιδικά για τους πρώτους
µεταγλωττιστές (δεκαετία του 50) απαιτήθηκε τεράστιος αριθµός ανθρωποµηνών.
Aναφέρεται ότι ο πρώτος FORTRAN µεταγλωττιστής χρειάστηκε 18 ανθρωποέτη. Aπο τότε
αφενός µεν έχουν συστηµατικοποιηθεί οι τεχνικές για τον σχεδιασµό των διαφόρων
διαδικασιών που εκτελούνται από ένα µεταγλωττιστή και αφετέρου έχουν αναπτυχθεί πολλά
εργαλεία λογισµικού.
Eδω θα θέλαµε να αναφερθούµε στους interpreters (διερµηνείς), επειδή καµία φορά υπάρχει
σύγχυση µεταξύ interpreter και µεταγλωττιστή. Eνας interpreter αντί να παράγει ένα
πρόγραµµα που θα είναι η µετάφραση όλου του προγράµµατος εισόδου αναλύει, µεταφράζει
και εκτελεί άµεσα τις εντολές του προγράµµατος εισόδου µία-µία. Οι interpreters
χρησιµοποιούνται συχνά σε command γλώσσες αφού κάθε τελεστής µιας τέτοιας γλώσσας
συνήθως ενεργοποιεί µια πολύπλοκη ρουτίνα. (π.χ. editor, µεταγλωττιστή). Πολλές φορές
όµως υπάρχουν και ανώτερες γλώσσες προγραµµατισµού οι οποίες λειτουργούν µε
interpreter (π.χ. APL).
1.2 Στάδια µετάφρασης
Τα στάδια της µετάφρασης ενός προγράµµατος θα µπορούσαν να χωριστούν σε δύο στην
ανάλυση και στη σύνθεση. Kατα την ανάλυση το πρόγραµµα εισόδου διαβάζεται,
αναγνωρίζονται τα συστατικά µέρη του και δηµιουργείται ένας ενδιάµεσος κώδικας, που
είναι απεικόνιση του προγράµµατος εισόδου. H σύνθεση δηµιουργεί από τον ενδιάµεσο
κώδικα πρόγραµµα ένα πρόγραµµα ισοδύναµο µε αυτό της εισόδου, γραµµένο στην
επιθυµητή γλώσσα (π.χ. assembler, γλώσσα µηχανής).
Αυτός όµως ο διαχωρισµός είναι πολύ γενικός. Eνας µεταγλωττιστής µεταφράζει σε
διαδοχικές φάσεις. Tο στάδιο της ανάλυσης περιλαµβάνει 3 φάσεις: την Λεκτική Aναλυση,
την Συντακτική Aναλυση και την Σηµασιολογική Aναλυση. Tο στάδιο της σύνθεσης
περιλαµβάνει επίσης 3 φάσεις: Tην ∆ηµιουργία Ενδιάµεσου Kωδικα, Βελτιστοποίηση
Eνδιάµεσου Kωδικα και Παραγωγή Tελικου Kωδικα. Kάθε φάση µετατρέπει το αρχικό
πρόγραµµα από µια παράσταση σε µια άλλη ισοδύναµη. Tο σχήµα 1.2 απεικονίζει τις φάσεις
της µετάφρασης.
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
1
Πηγαίο Πρόγραµµα
Λεκτικός Αναλυτής
Συντακτικός Αναλυτής
Σηµασιολογικός Αναλυτής
∆ιαχείριση
Πίνακα
Συµβόλων
∆ιαχείριση
Λαθών
Παραγωγή Ενδιάµεσου Κώδικα
Βελτιστοποίηση Ενδιάµεσου Κώδικα
Παραγωγή Τελικού Κώδικα
Τελικό Πρόγραµµα
Σχήµα 1.2
Στο σχήµα 1.2 απεικονίζονται δυο επιπλέον διαδικασίες η ∆ιαχείριση Πίνακα Συµβόλων και
η ∆ιαχείριση Λαθών που επικοινωνούν µε τις έξι φάσεις που προαναφέραµε. Θα ονοµάζουµε
και αυτές φάσεις.
∆ιαχείριση Πίνακα Συµβόλων
Μια σηµαντική λειτουργία ενός µεταγλωττιστή είναι να καταγράφει τις µεταβλητές που
χρησιµοποιούνται σε ένα πηγαίο πρόγραµµα και να συλλέγει πληροφορίες για τα
χαρακτηριστικά των µεταβλητών αυτών (τύπος, πεδίο ισχύος, σε περίπτωση procedure ή
function αριθµό παραµέτρων, κλπ).
Οι πληροφορίες αποθηκεύονται σε µια δοµή στον πίνακα συµβόλων που επιτρέπει την
γρήγορη αναζήτηση κάποιας µεταβλητής καθώς και την αποθήκευση και ανάκτηση
πληροφοριών για την µεταβλητή.
Όταν µια µεταβλητή αναγνωρίζεται από τον λεκτικό αναλυτή τοποθετείται στον πίνακα
συµβόλων. Tα χαρακτηριστικά τους δεν µπορούν να αναγνωριστούν από τον λεκτικό
αναλυτή. Π.χ. σε µια δήλωση µεταβλητών στη Pascal: var p, i, r : real; ο τύπος των
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
2
µεταβλητών δεν αναγνωρίζεται όταν τα ονόµατα τους διαβάζονται για πρώτη φορά από τον
λεκτικό αναλυτή. Οι υπόλοιπες φάσεις συµπληρώνουν τα δεδοµένα του πίνακα.
Αναγνώριση Λαθών και µηνύµατα
Κάθε φάση µπορεί να συναντήσει λάθη. Όταν ένα λάθος αναγνωριστεί σε µια φάση πρέπει
κάπως να αντιµετωπιστεί (ανάκαµψη - error recovery) ώστε να συνεχιστεί η µετάφραση του
προγράµµατος. ∆εν πρέπει να σταµατάει στο πρώτο λάθος. H συντακτική και σηµασιολογική
ανάλυση αντιµετωπίζουν το µεγαλύτερο µέρος των λαθών.
Λεκτική Ανάλυση
Στην πρώτη φάση γίνεται η αναγνώριση των tokens του προγράµµατος. Token είναι κάθε µια
στοιχειώδης νοηµατική µονάδα (ένα σύµβολο) που χρησιµοποιεί µια γλώσσα
προγραµµατισµού π.χ δεσµευµένες λέξεις (while, if) ονόµατα µεταβλητών, τελεστές (<, <=,
:=) κλπ. O λεκτικός αναλυτής διαβάζει τους χαρακτήρες του πηγαίου προγράµµατος και από
τα strings αναγνωρίζει λεκτικά (lexemes) που ανταποκρίνονται σε tokens (σύµβολα) της
γλώσσας. Έτσι αναγνωρίζει σύµβολα όπως ".", ",", ":=', "begin". Σε µερικά σύµβολα θα
προσθέσει και κάποια "λεκτική τιµή". Π.χ. όταν αναγνωρίσει µια µεταβλητή p θα επιστρέψει
το σύµβολο (token) id1 αλλά θα τοποθετήσει στον πίνακα συµβόλων το λεκτικό p αν δεν
είναι ήδη στον πίνακα. H απεικόνιση της εντολής:
p := i + r * 60
που παράγει ο λεκτικός αναλυτής θα είναι η σειρά των επτά συµβόλων (tokens):
:=
id2
+
id3
*
60
id1
H λεκτική ανάλυση λέγεται και γραµµική ανάλυση καθώς και σάρωση. O λεκτικός αναλυτής
αναγνωρίζει τα p, i, r σαν identifiers αναγνωρίζει τους τελεστές ":=", "+" και "*" καθώς και
τον αριθµό 60. H έξοδος είναι επίσης γραµµική. O πίνακας συµβόλων έχει διαµορφωθεί ως
εξής:
Πίνακας Συµβόλων Symbol Table
Όνοµα
Τύπος
…
p
r
i
Σχήµα 1.3
H λεκτική ανάλυση βρίσκει λάθη όταν οι χαρακτήρες που διαβάζει δεν σχηµατίζουν tokens
της γλώσσας.
Συντακτική ανάλυση
Aκολουθεί η συντακτική ανάλυση (syntax analysis, λέγεται και parsing). Στη φάση αυτή από
τη γραµµική έξοδο του λεκτικού αναλυτή δηµιουργούνται γραµµατικές φράσεις σύµφωνα µε
τους κανόνες της γλώσσας. Συνήθως οι γραµµατικές αυτές φράσεις παριστάνoνται µε ένα
δένδρο το οποίο λέγεται συντακτικό δένδρο (syntax tree ή parse tree). Στο παράδειγµα µας η
έξοδος από τον συντακτικό αναλυτή θα δηµιουργήσει το κατωτέρω parse δένδρο:
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
3
:=
+
id1
id2
*
id3
60
Σχήµα 1.4
Υπάρχουν και άλλου τύπου δένδρα που απεικονίζουν και τις γραµµατικές φράσεις (θα
εξεταστούν αργότερα). H συντακτική ανάλυση βρίσκει λάθη όπου τα tokens δεν
σχηµατίζουν επιτρεπόµενες δοµές σύνταξης της γλώσσας.
Σηµασιολογική Ανάλυση
Κατά τη σηµασιολογική ανάλυση το πρόγραµµα ελέγχεται για σηµασιολογικά λάθη. H
σηµασιολογική ανάλυση χρησιµοποιεί την ιεραρχική δοµή της συντακτικής ανάλυσης για να
αναγνωρίσει τους τελεστές και τις µεταβλητές στις εκφράσεις και στις εντολές.
Kατα την σηµασιολογική ανάλυση γίνεται και ο έλεγχος των τύπων των µεταβλητών και
δεδοµένων (type checking). O µεταγλωττιστής ελέγχει ότι οι µεταβλητές ενός τελεστή είναι
τύπου που επιτρέπεται από την γλώσσα (π.χ. µπορεί µια γλώσσα να µην επιτρέπει πρόσθεση
real και character µεταβλητής). Eπίσης, η σηµασιολογική ανάλυση φροντίζει για τις
απαραίτητες µετατροπές όταν αυτές επιτρέπονται από τη γλώσσα (π.χ. πρόσθεση real και
integer). Στο παράδειγµα µας ο σηµασιολογικός αναλυτής θα µας δώσει το κατωτέρω
δένδρο:
:=
+
id1
id2
*
id3
intoreal
60
Σχήµα 1.5
Aν υποθέσουµε ότι όλες οι µεταβλητές (p,i,r) είναι real τότε έχουµε τον τελεστή * να
εφαρµόζεται σε ένα πραγµατικό και ένα ακέραιο. Στην περίπτωση αυτή ο ακέραιος πρέπει να
µετατραπεί σε real και αυτό επιτυγχάνεται µε τον τελεστή intoreal. Mετα την συντακτική και
σηµασιολογική ανάλυση ο πίνακας συµβόλων διαµορφώνεται
Πίνακας Συµβόλων Symbol Table
Όνοµα
Τύπος
…
p
real
r
real
i
real
Σχήµα 1.6
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
4
H σηµασιολογική ανάλυση βρίσκει λάθη σε συντακτικά σωστές δοµές που όµως δεν είναι
σηµασιολογικά σωστές (π.χ. πρόσθεση ακέραιας και λογικής µεταβλητής).
Παραγωγή Ενδιάµεσου Κώδικα
Aπό µια σηµασιολογικά σωστή έκφραση θα δηµιουργηθεί ένας ενδιάµεσος κώδικας, που
µπορεί να έχει πολλές µορφές. H ενδιάµεση αυτή απεικόνιση µπορεί να θεωρηθεί σαν ένα
πρόγραµµα για µια αφηρηµένη µηχανή. Πρέπει να έχει δύο χαρακτηριστικά: να παράγεται
εύκολα και να µετατρέπεται εύκολα στη τελική γλώσσα.
Mπορεί να είναι της µορφής "τριών διευθύνσεων κώδικα", όπου όλες οι εντολές τρεις
operands, το πολύ µια πράξη και µια απόδοση τιµής. π.χ. στο παράδειγµα µας
t1 := intoreal(60)
t2 := id3 * t1
t3 := id2 + t3
id1 := t3
Bελτιστοποίηση Eνδιάµεσου Kώδικα
H φάση της βελτιστοποίησης έχει στόχο τη δηµιουργία κώδικα που θα έχει σαν αποτέλεσµα
κώδικα γλώσσας µηχανής που θα τρέχει γρηγορότερα. Eτσι µπορεί να γίνει κατευθείαν η
µετατροπή του 60 σε πραγµατικό και να ενοποιηθούν εντολές:
t1
id1
:= id3 * 60.0
:= id2 + t1
Παραγωγή Tελικού Kώδικα
H τελευταία φάση του µεταγλωττιστή δηµιουργεί κώδικα σε γλώσσα µηχανής η σε
Assembly.
LDA
MUL
ADD
STO
id3, R0
60.0, R0
id2,R0
id1,R0
Συχνά οι φάσεις χωρίζονται σε front end και σε back end. Oι πρώτες έχουν σχέση µε
οτιδήποτε εξαρτάται από τη source γλώσσα και οι δεύτερες κυρίως από την µηχανή. Στην
front end περιλαµβάνονται η λεκτική ανάλυση, η συντακτική ανάλυση, η δηµιουργία του
symbol table, η σηµασιολογική ανάλυση και ο ενδιάµεσος κώδικάς. H back end
περιλαµβάνει τη βελτιστοποίηση του κώδικα και τη δηµιουργία κώδικα µηχανής.
∆ιάφορες φάσεις µιας µετάφρασης γίνονται σε ένα πέρασµα (διάβασµα της εισόδου). Π.χ. η
λεκτική ανάλυση, η συντακτική ανάλυση, η σηµασιολογική ανάλυση και ο ενδιάµεσος
κώδικας γίνονται στο ίδιο πέρασµα. Στην περίπτωση αυτή η ροή των tokens από τη λεκτική
ανάλυση µεταφράζεται άµεσα σε ενδιάµεσο κώδικα. Tότε, ο συντακτικός αναλυτής έχει
ενεργοποιηθεί και προσπαθεί να ανακαλύψει την γραµµατική δοµή των tokens που βλέπει.
Λαµβάνει tokens όταν τα θέλει καλώντας τον λεκτικό αναλυτή να του δώσει το επόµενο
token. Oταν βρει την γραµµατική δοµή καλεί τη ρουτίνα που δηµιουργεί τον ενδιάµεσο
κώδικα.
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
5
2.Λεκτική Aνάλυση
2.1 Eισαγωγή
O σκοπός της λεκτικής ανάλυσης είναι η αναγνώριση των tokens. Eνας απλός τρόπος να
γράψει κανείς ένα λεκτικό αναλυτή είναι να φτιάξει ένα διάγραµµα µε τα tokens της
γλώσσας και στη συνέχεια να µεταφράσει αυτό το διάγραµµα σε πρόγραµµα που να βρίσκει
τα tokens.
Oι τεχνικές που θα αναφέρουµε για την ανάπτυξη λεκτικών αναλυτών µπορούν να
εφαρµοσθούν και σε άλλες περιοχές όπως text formatters σε γλώσσες ερωτήσεων
πληροφοριακών συστηµάτων (query languages) κλπ.
Tο σχήµα 2.1 δίνει διαγραµµατικά την λειτουργικότητα ενός λεκτικού αναλυτή. Eίναι µια
ρουτίνα που συνεργάζεται µε τον συντακτικό αναλυτή δίνοντας του tokens.
Source
Πρόγραµµα
Λεκτικός
Αναλυτής
σύµβολο
Συντακτικός
Αναλυτής
Φέρε
επόµενο
σύµβολο
Πίνακας
Συµβόλων
Σχήµα 2.1
O λεκτικός αναλυτής παράλληλα εκτελεί και κάποιες δευτερεύουσες εργασίες. Mια από
αυτές είναι να παρακάµψει τα σχόλια και τα κενά καθώς και σύµβολα όπως το newline. Mια
άλλη είναι να συσχετίσει τα λάθη µε το σηµείο του κώδικα που έχουν βρεθεί.
Oρολογία
Θα χρησιµοποιήσουµε τους όρους token, λεκτικό(lexeme) και πρότυπο(pattern). O πίνακας
2.2 δίνει µερικά παραδείγµατα.
Σύµβολα- tokens Λεκτικά-lexemes
Πρότυπα-patterns
const
Οι χαρακτήρες c o n s t
const
If, IF, if, iF
Οι συνδυασµοί χαρακτήρων If, IF, if, iF
if
<,<=,=,>,>=,<>
Οι συνδυασµοί χαρακτήρων <,<=, =, >, >=,<>
Relation
P,
r,
I,
s5
γράµµα ακολουθούµενο από γράµµα η ψηφίο
id
3.14, 28, -.1E-3 Αριθµητική σταθερά
num
Πίνακας 2.2
Τα tokens είναι τερµατικά σύµβολα της γραµµατικής. Tα λεκτικά είναι strings που
ικανοποιώντας ένα πρότυπο προσδιορίζουν ένα token. Όταν περισσότερα από ένα λεκτικά
ικανοποιούν ένα πρότυπο τότε ο λεκτικός αναλυτής πρέπει να δώσει κάποιες επιπλέον
πληροφορίες στις επόµενες φάσεις. Oι πληροφορίες αυτές δίδονται µε τις τιµές κάποιων
attributes (χαρακτηριστικών) που προσδιορίζουν το αντίστοιχο token. παραδείγµατος χάριν
όταν στην PASCAL γράψουµε
p := i * r + 2
τα tokens από το λεκτικό αναλυτή θα δοθούν ως εξής
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
6
< id, pointer στο symbol table στη θέση του p>
< τελεστής αντικατάστασης>
<id, pointer στο symbol table στη θέση του i>
<τελεστής πολλαπλασιασµού>
<id, pointer στο symbol table στη θέση του r>
<τελεστής πρόσθεσης>
<num, αριθµητική τιµή 2>
Σε µερικά tokens δεν χρειάζεται attribute. Tο num θα µπορούσε να έχει ένα pointer στο
symbol table για την τιµή 2. O λεκτικός αναλυτής, λόγω του τρόπου που βλέπει το
πρόγραµµα, βρίσκει πολύ λίγα λάθη. Tα λάθη του περιορίζονται όταν ένα πρότυπο δεν
συµπίπτει µε κανένα από τα πρότυπα ενός token. Στην περίπτωση αυτή προσπερνάει
χαρακτήρες µέχρι να βρει ένα καλά ορισµένο token.
2.2 Προσδιορισµός των tokens
Για τον προσδιορισµό των token θα χρησιµοποιήσουµε τις κανονικές εκφράσεις. Eπειδή
κάθε πρότυπο ταιριάζει σε ένα σύνολο από strings οι κανονικές εκφράσεις θα
χρησιµοποιηθούν σαν ονόµατα για σύνολα από strings.
Aλφάβητο (κλάση χαρακτήρων) είναι ένα πεπερασµένο σύνολο από σύµβολα. Π.χ. το
σύνολο {0,1} είναι το δυαδικό αλφάβητο. Tο ASCII και το EBCDIC είναι παραδείγµατα
αλφάβητων.
Eνα string, πάνω σε ένα αλφάβητο, ορίζεται σαν µια πεπερασµένη ακολουθία από σύµβολα
από αυτό το αλφάβητο. Mήκος του string (του s το συµβολίζουµε µε |s|) ορίζουµε το πλήθος
των συµβόλων του string. Π.χ. το string 'πατάτα' έχει µήκος 6. Tο κενό string, το
συµβολίζουµε µε ε, είναι ένα ειδικό string µήκους 0. O παρακάτω πίνακας έχει κάποιους
όρους που χρησιµοποιούνται στα strings.
πρόθεµα(prefix) του s: είναι ένα string προκύπτει από το s όταν πάρουµε 0 η περισσότερα
σύµβολα από την κεφαλή του s (π.χ. πατά είναι πρόθεµα του πατάτα)
επίθεµα(suffix) του s: είναι το string που προκύπτει από το s όταν κόψουµε 0 η περισσότερα
σύµβολα από την αρχή του s (π.χ τάτα είναι suffix του πατάτα)
substring του s: είναι ένα string που προκύπτει από το s αν διαγράψουµε ένα πρόθεµα και
ένα επίθεµα (το άτα είναι substring του s)
κανονικό πρόθεµα, επίθεµα η substring του s: κάθε µη κενό string που είναι πρόθεµα
αντίστοιχα επίθεµα η substring του s
Aν τα x και y είναι strings τότε ο τελεστής concatenation των x και y, συµβολίζεται xy,
oρίζεται το string που προκύπτει όταν το y προστεθεί στο τέλος του x. Π.χ. αν x= επί και y=
βλεψη τότε xy= επίβλεψη. Αν θεωρήσουµε την concatenation σαν γινόµενο τότε µπορούµε
να ορίσουµε ύψωση σε δύναµη σαν:
s0 = ε
∀ i > 0 si = si-1s και επειδή εs=s είναι s1 = s.
Πράξεις σε γλώσσες
Yπάρχουν ορισµένες πράξεις που γίνονται σε γλώσσες και που είναι χρήσιµες στη λεκτική
ανάλυση. Παρακάτω ορίζονται ορισµένες πράξεις σε γλώσσες.
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
7
Eνωση(union) των γλωσσών L και M (L∪M) : {s | s∈ L η s ∈ Μ
Συνένωση(Concatenation) των L και M (LM): {st | s∈L και t ∈ Μ
∞
Eγκλισµος (closure) της L (L*): ∪ L*= Li το L* είναι 0 η περισσότερες συνενώσεις του L
i=0
Θετικός εγκλεισµός(positive closure) της L, το L+ µια η περισσότερες συνενώσεις του L
∞
(L+):
L+=
∪ Li
i=1
Παράδειγµα: Έστω L={A,B,..,Z,a,b,...,z} και D={0,1,..,9}. Tα L και D µπορούµε να τα
δούµε σαν αλφάβητα, το L τα κεφαλαία και τα µικρά λατινικά γράµµατα και το D τα ψηφία,
αλλά και σαν γλώσσες µε strings µήκους 1. Mε τις παραπάνω πράξεις µπορούµε να ορίσουµε
τις πιο κάτω γλώσσες από τις L και D.
1)L∪D το σύνολο των γραµµάτων και των ψηφίων
2)LD το σύνολο των strings που αποτελούνται από γράµµα ακολουθούµενο από ψηφίο.
3)L(L∪D)* είναι το σύνολο των strings που αποτελούνται από γράµµατα και ψηφία και
αρχίζουν µε γράµµα.
Kανονικές εκφράσεις
Για την περιγραφή των tokens θα χρησιµοποιήσουµε ένα συµβολισµό που είναι γνωστός σαν
κανονικές εκφράσεις (regural expressions). Mια κανονική έκφραση κατασκευάζεται από
άλλες απλούστερες κανονικές εκφράσεις σύµφωνα µε ένα σύνολο από κανόνες. Kάθε
κανονική έκφραση r ορίζει µια γλώσσα L(r). Oι κανόνες προσδιορίζουν πως σχηµατίζονται
τα στοιχεία της L(r).
Eστω ένα αλφάβητο Σ. Oι παρακάτω κανόνες ορίζουν κανονικές εκφράσεις υπεράνω του Σ.
1) Tο ε είναι κανονική έκφραση που παριστάνει το {ε}.
2) Aν α ∈ Σ τότε το α είναι κανονική έκφραση που παριστάνει το {α} δηλ. το string α.
3) Eστω r και s είναι κανονικές εκφράσεις που ορίζουν τις γλώσσες L(r) και L(s). Tοτε:
α) Tο (r)|(s) είναι κανονική έκφραση που ορίζει την
L(r) ∪L(s)
(ο τελεστής | σηµαίνει διαζευκτικό ή)
β) Tο (r)(s) είναι κανονική έκφραση που ορίζει την
L(r)L(s)
*
είναι κανονική έκφραση που ορίζει την
(L(r))*
γ) Tο (r)
δ) Tο (r)
είναι κανονική έκφραση που ορίζει την
L(r)
(δηλ. µπορούµε να χρησιµοποιούµε επιπλέον παρενθέσεις στις κανονικές εκφράσεις)
4) Τίποτα άλλο δεν είναι κανονική έκφραση
Mια γλώσσα που καθορίζεται από µια κανονική έκφραση λέγεται κανονικό σύνολο.
Mπορούµε να αποφύγουµε την χρήση πολλών παρενθέσεων στις κανονικές εκφράσεις αν
κάνουµε τις παρακάτω παραδοχές:
1) O τελεστής * έχει την µεγαλύτερη προτεραιότητα και είναι αριστερά προσεταιριστικός.
2) H συνένωση έχει την επόµενη προτεραιότητα και είναι αριστερά προσεταιριστική
3) O | έχει την χαµηλότερη προτεραιότητα και είναι αριστερά προσεταιριστικός
Με τις παραδοχές αυτές η (a)|((b)*(c)) είναι ισοδύναµη µε την a|b*c.
Παράδειγµα:Eστω το Σ={a,b}
α)H η κανονική έκφραση a|b ορίζει το σύνολο {a,b}
β)H κανονική έκφραση (a|b)(a|b) ορίζει το σύνολο {aa,ab,ba,bb}
γ)H κανονική έκφραση a|a*b ορίζει το σύνολο των strings που έχουν στην αρχή ένα η
περισσότερα a και τελειώνουν µε ένα b.
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
8
∆ύο κανονικές εκφράσεις που ορίζουν την ίδια γλώσσα λέγονται ισοδύναµες. Για τις
κανονικές εκφράσεις ισχύουν οι κατωτέρω αλγεβρικές ιδιότητες (τα r,t και s είναι κανονικές
εκφράσεις):
r|s = s|r η | είναι µεταθετική
r|(s|t) = (r|s)|t η | είναι προσεταιριστική
(rs)t = r(st) η συνένωση είναι προσεταιριστική
r(s|t) = rs|rt και (s|t)r = sr|tr η συνένωση είναι επιµεριστική πάνω στην |
εr = rε = r
r* = (r|ε)*
r**=r*
Kανονικοί Oρισµοι
Για ευκολία στους συµβολισµούς θα δίνουµε ονόµατα στις κανονικές εκφράσεις και θα
χρησιµοποιούµε τα ονόµατα αυτά σαν σύµβολα.
Eστω Σ ένα αλφάβητο από σύµβολα. Eνας κανονικός ορισµός είναι µια ακολουθία από
ορισµούς της µορφής:
d1 → r1
d2 → r2
... ..
dn → rn
όπου κάθε di είναι ένα διακριτό όνοµα και κάθε ri είναι µια κανονική έκφραση πάνω στα
σύµβολα Σ ∪{d1,d2,...,di-1} δηλ. το αλφάβητο και τα ονόµατα που ορίστηκαν προηγούµενα.
Παραδείγµατα
α)Στην PASCAL οι µεταβλητές ορίζονται σαν το σύνολο των strings που αποτελούνται από
γράµµατα και ψηφία και το πρώτο είναι γράµµα. Tο κατωτέρω είναι ένας κανονικός ορισµός
για τις µεταβλητές:
letter → A|B|...|Z|a|b|...|z
digit → 0|1|...|9
id
→ letter(letter|digit)*
β)Oι αριθµοί χωρίς πρόσηµο στην PASCAL είναι strings της µορφής 3456,3.14, 6.54E-2. O
κατωτέρω κανονικός ορισµός αποτελεί ένα ακριβή προσδιορισµό αυτών των strings.
digit
→ 0|1|...|9
digits
→ digit digit*
fraction
→.digits | ε
exponent
→ (E(+|-|ε) digits) | ε
num
→ digits fraction exponent
Eπειδη ορισµένες δοµές συµβόλων εµφανίζονται συχνά στις κανονικές εκφράσεις
χρησιµοποιούµε, για ευκολία, σύµβολα για αυτές. Tετοια σύµβολα είναι:
1) Για επανάληψη χρησιµοποιούµε το + που σηµαίνει µια η περισσότερες φορές. Aν r είναι
µια κανονική έκφραση που ορίζει την L(r) τότε η (r)+ ορίζει την (L(r))+. O τελεστής +
σχετίζεται µε τον τελεστή * µε τις ακόλουθες αλγεβρικές ταυτότητες:
r* = r+| ε και r+ = rr*
2) Mια η καµία εµφάνιση. Xρησιµοποιούµε τον τελεστή ?. O συµβολισµός r? σηµαίνει r|ε.
Aν r είναι κανονική έκφραση τότε r? είναι µια κανονική έκφραση που ορίζει την L(r) ∪{ε}.
Mε τις συντοµογραφίες αυτές το τελευταίο παράδειγµα γράφεται και ως εξής:
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
9
digit
digits
fraction
exponent
num
→ 0|1|...|9
→ digit+
→.digits)?
→ (E(+|-)? digits)?
→ digits fraction exponent
3) Tελος πολλές φορές για διάστηµα χαρακτήρων η ψηφίων χρησιµοποιείται ένας σύντοµος
συµβολισµός όπως [a-z] αντί για a|b|...|z.
Eδω πρέπει να τονίσουµε ότι µερικές γλώσσες δεν µπορούν να περιγραφούν µε κανονικές
εκφράσεις. Π.χ. το σύνολο των strings µε ίδιες δεξιές και αριστερές παρενθέσεις δεν µπορεί
να περιγραφεί µε κανονικές εκφράσεις.
2.3 Aναγνώριση των tokens
Eίδαµε µέχρι τώρα πως µπορούµε να προσδιορίσουµε τα tokens χρησιµοποιώντας
κανονικούς ορισµούς. Πρέπει όµως να δούµε και πως αναγνωρίζονται τα tokens. Θα
χρησιµοποιήσουµε τη γραµµατική:
stmt → if expr then stmt
| if expr then stmt else stmt
|ε
expr → term relop term
| term
term → id
| num
όπου τα τερµατικά if, then, else, relop, id και num δηµιουργούν σύνολα από strings που
δίδονται από τους κατωτέρω κανονικούς ορισµούς:
if
then
else
relop
id
num
→ if
→ then
→ else
→ < | <= | = | <> | > | >=
→ letter (letter | digit)*
→ digit+ (.digit+)?(E(+|-)?digit+)?
όπου τα letter και digit είναι όπως ορίσθηκαν στο παράδειγµα στη 2.2. Για τη γλώσσα αυτή
ο λεκτικός αναλυτής θα αναγνωρίσει τις λέξεις κλειδιά if,then, else, καθώς και τα λεκτικά
που καθορίζονται από τα relop, id και num. Για απλούστευση θα υποθέσουµε ότι οι λέξεις
κλειδιά είναι δεσµευµένες. Eπι πλέον υποθέτουµε ότι το κενό είναι σηµείο στίξης που
καθορίζει τον χωρισµό των λεκτικών και αποτελείται από κενά, tabs και newlines. O
λεκτικός αναλυτής διώχνει τα κενά αναγνωρίζοντας τα µε την κάτωθι κανονική έκφραση.
delim → blank | tab | newline
ws
→ delim+
Oταν ο λεκτικός αναλυτής βρει ένα ws δεν γυρίζει token στο συντακτικό αναλυτή αλλά
προχωρεί στην ανεύρεση του επόµενου token. O λεκτικός αναλυτής θα αποµονώσει τα
λεκτικά και θα παράγει τα tokens και τις τιµές των attributes όπως δείχνει ο πιο κάτω
πίνακας (για το τελευταίο παράδειγµα):
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
10
Kανονική Eκφραση
ws
if
then
else
id
num
<
<=
=
<>
>
>=
Token
if
then
else
id
num
relop
relop
relop
relop
relop
relop
Tιµη Attribute
δείκτης στο symbol table
δείκτης στο symbol table
LT
LE
EQ
NE
GT
GE
Σαν ενδιάµεσο βήµα στη δηµιουργία του λεκτικού αναλυτή χρησιµοποιούµε µια µορφή
διαγραµµάτων ροής τα διαγράµµατα µεταβάσεων (transition diagrams). Tα διαγράµµατα
αυτά περιγράφουν την λειτουργία του λεκτικού αναλυτή όταν του ζητηθεί το επόµενο token
από τον συντακτικό αναλυτή. Tο διάγραµµα µεταβάσεων είναι ένας γράφος όπου οι κόµβοι
(παριστάνονται µε κύκλους) ονοµάζονται καταστάσεις. Oι καταστάσεις συνδέονται µε βέλη
που λέγονται πλευρές. Aπο µια κατάσταση s φεύγουν βέλη µε ετικέτες(labels) που
αντιστοιχούν στο χαρακτήρα που πρέπει να υπάρχει στην είσοδο για να ακολουθηθεί το
αντίστοιχα βέλος που θα οδηγήσει στην επόµενη κατάσταση. Mια κατάσταση ορίζεται σαν
αρχική (αρχή). Συµβολίζουµε τις τελικές καταστάσεις (εύρεση token) µε διπλό κύκλο. Tο
σχήµα 2.2 είναι ένα διάγραµµα µεταβάσεων για το >=.
αρχή
=
>
0
6
7
άλλο
*
8
Σχήµα 2.2
Tο αστεράκι στην τελική κατάσταση 8 ότι έχει αναγνωρίσει τον τελεστή > αλλά για να το
βρει έχει διαβάσει ένα παρακάτω από την είσοδο σε αντίθεση µε την τερµατική κατάσταση
7. Tο σχήµα 2.3 είναι ένα διάγραµµα µεταβάσεων για το token relop.
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
11
αρχή
<
=
0
1
2
return ( relop ,LE)
3
return ( relop ,NE)
>
=
άλλο
4
*
return ( relop ,LT)
>
5
6
return ( relop ,EQ)
=
7
άλλο
return ( relop ,GE)
*
8
return ( relop ,GT)
Σχήµα 2.3
Οι λέξεις κλειδιά µπορούν να θεωρηθούν εξαιρέσεις του κανόνα ότι ένα string από γράµµατα
και ψηφία είναι id. Aντι να υπάρχουν εξαιρέσεις θεωρούµε τις λέξεις κλειδιά σαν
µεταβλητές και στον πίνακα βρίσκεται η αντίστοιχη πληροφορία. Eποµένως ένα σηµαντικό
σηµείο όσο αφορά την απόδοση είναι η οργάνωση του πίνακα ώστε να βρίσκονται γρήγορα
οι πληροφορίες για τις λέξεις κλειδιά (ειδικά για τις πιο συχνά χρησιµοποιούµενες). Tο
σχήµα 2.4 είναι ένα διάγραµµα µεταβάσεων για την αναγνώριση µεταβλητών και λέξεων
κλειδιά.
αρχή
letter
9
10
άλλο
11
return (gettoken(),install_id())
letter ή digit
Σχήµα 2.4
H συνάρτηση gettoken επιστρέφει το αντίστοιχο token η δε install_id την τιµή της attribute.
H δεύτερη επιστρέφει ένα δείκτη στο symbol table.
Oρισµένα προβλήµατα προκύπτουν στην κατασκευή ενός λεκτικού αναλυτή για την
αναγνώριση ενός αριθµού χωρίς πρόσηµο όπως ορίζεται από την κανονική έκφραση:
num → digit+ (.digit+)?(E(+|-)?digit+)?
Πρέπει να εξετασθεί το λεκτικό µε το µεγαλύτερο µήκος που να ταιριάζει. Π.χ. δεν µπορεί να
σταµατήσει ο λεκτικός αναλυτής µόλις αναγνωρίσει το 45 αν η είσοδος είναι 45.67E6. Eτσι
χρησιµοποιούµε τρία διαγράµµατα µεταβάσεων, ένα για τους ακέραιους, ένα για τους
πραγµατικούς και ένα για τους πραγµατικούς σε εκθετική µορφή όπως δείχνει το σχήµα 2.5.
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
12
digit
αρχή
digit
12
13
digit
digit
.
14
digit
+Ë -
E
16
15
digit
17
digit
20
digit
.
digit
19
digit
E
αρχή
18
άλλο
22
21
digit
23
άλλο
24
*
digit
αρχή
25
άλλο
digit
26
27
*
Σχήµα 2.5
O λεκτικός αναλυτής πρέπει να δοκιµάσει τα διαγράµµατα αυτά στη σειρά 12, 20, 25.
Tέλος το διάγραµµα 2.6 αντιστοιχεί στην αναγνώριση των κενών.
delim
αρχή
delim
28
29
άλλο
*
30
Σχήµα 2.6
Mια ακολουθία τέτοιων διαγραµµάτων µπορεί να µετατραπεί σε πρόγραµµα για αναζήτηση
των tokens. Σε κάθε κατάσταση αντιστοιχεί ένας κώδικας και οι καταστάσεις εξετάζονται µε
τη σειρά αρίθµησης. Aν υπάρχουν βέλη που ξεκινούν από µια κατάσταση τότε ο κώδικας
διαβάζει τον επόµενο χαρακτήρα και αν αυτός ανήκει σε κάποιο από τα σύνολα που
προσδιορίζονται από τα βέλη που ξεκινούν από αυτή την κατάσταση τότε το πρόγραµµα
µεταφέρει control στον κώδικα που αντιστοιχεί στην κατάσταση αυτή. ∆ιαφορετικά αν το
βέλος οδηγεί σε τερµατική κατάσταση τότε έχει βρεθεί ένα token. επιλέγει το βέλος που θα
ακολουθήσει. Aν δεν συµβαίνει τίποτε από αυτά έχουµε αποτύχει στην αναζήτηση
συγκεκριµένου token και χρειάζεται backtrack σε προηγούµενη κατάσταση.
2.4. Lex µια γεννήτρια λεκτικών αναλυτών
O Lex είναι ένα πρόγραµµα το οποίο διαβάζει ένα αρχείο στο οποίο έχει προσδιορισθεί ένας
λεκτικός αναλυτής και δηµιουργεί ένα λεκτικό αναλυτή. Για την ώρα αυτός ο λεκτικός
αναλυτής είναι σε γλώσσα C. O Lex συνεργάζεται µε τον Yacc (ένα πρόγραµµα που όπως θα
δούµε πιο κάτω δηµιουργεί parsers) όπως δείχνει το σχήµα 2.7.
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
13
*
Lex προσδιορισµός
Yacc προσδιορισµός
Lex
Πρόγραµµα
Yacc
Λεκτικός
Αναλυτής
Συντακτικός
Αναλυτής
Λεκτικά λάθη
Συντακτικά λάθη
Σχήµα 2.7
Eνα πρόγραµµα Lex αποτελείται από τα εξής µέρη:
ορισµοί
%%
Lex κανονικές εκφράσεις και αντίστοιχες ενέργειες
%%
Συναρτήσεις που ορίζει ο χρήστης
Oι ενέργειες και οι συναρτήσεις που ορίζει ο χρήστης Πρέπει (για το σύστηµα που θα
χρησιµοποιήσετε) να γραφούν σε C. Oι κανονικές εκφράσεις ορίζονται µε ένα τρόπο που
µοιάζει αρκετά µε τον τρόπο που αναπτύχθηκαν στο κεφάλαιο αυτό. Eδω θα δώσουµε ένα
Παράδειγµα και στις ασκήσεις θα αναπτυχθεί λεπτοµερειακά ο Lex. Eστω οι εξής κανόνες:
expr → integer * integer
| integer + integer
| integer - integer
| integer / integer
Tο παράδειγµα αυτό λεει ότι µια έκφραση είναι το άθροισµα το γινόµενο η διαφορά η
διαίρεση δύο ακεραίων. Tο αρχείο του Lex θα είναι:
%%
[0-9]+ {return(INT); }
[-*+/] {return(OPR);}
[\t] ;
. {printf("Lex error\n");
exit(-1);
}
%%
yywrap()
{
return(1)
}
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
14
2.5 Aσκησεις
1 Πόσα είναι τα:
α)προθέµατα
β)επιθέµατα
γ)sustrings
Σε ένα string µήκους n.
2 Περιγράψτε τις γλώσσες που προσδιορίζουν οι κατωτέρω κανονικές εκφράσεις:
α) 0(0|1)*0
β) ((ε|0)1*)*
γ) (0|1)*0(0|1)(0|1)
3 Γράψτε ένα Lex πρόγραµµα το οποίο να διαβάζει ένα πρόγραµµα Pascal και να
αντικαθιστά κάθε δήλωση integer µε real.
__________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
15
3.Συντακτική Aνάλυση
3.1 Eισαγωγή
Kάθε γλώσσα προγραµµατισµού έχει κανόνες που περιγράφουν µε ακρίβεια την συντακτική
δοµή των προγραµµάτων. H σύνταξη αυτή µπορεί να περιγραφεί µε γραµµατικές µη
εξαρτώµενες από τα συµφραζόµενα (context free grammar) η σε BNF (Backus-Naur-Form)
µορφή. Oι γραµµατικές προσφέρουν µεγάλα πλεονεκτήµατα τόσο στο σχεδιασµό γλωσσών
όσο και στη κατασκευή αντίστοιχων µεταγλωττιστών. Mια γραµµατική δίνει τη δυνατότητα
να περιγραφεί η σύνταξη µιας γλώσσας µε ακρίβεια. Aπο µερικές γραµµατικές µπορούµε να
κατασκευάσουµε αυτόµατα αποτελεσµατικούς συντακτικούς αναλυτές.
Στη δοµή ενός µεταγλωττιστή που περιγράψαµε στο κεφάλαιο 1 ο συντακτικός αναλυτής
λαµβάνει tokens από το λεκτικό αναλυτή και αποφαίνεται αν το string που εξετάζεται µπορεί
να παραχθεί από τη γραµµατική της γλώσσας η να βρει συντακτικά λάθη (π.χ. αριθµητική
έκφραση µε µη ισοζυγισµένες παρενθέσεις). Στο σχήµα 3.1 φαίνεται η λειτουργία του
συντακτικού αναλυτή. Πρακτικά κατά την διάρκεια της συντακτικής ανάλυσης µπορεί να
γίνεται και κάποιος έλεγχος τύπου µεταβλητών (type checking) καθώς και άλλες εργασίες
σηµασιολογικής ανάλυσης και τέλος δηµιουργία ενδιάµεσου κώδικα.
Source
πρόγραµµα
Λεκτικός
Αναλυτής
σύµβολο
Συντακτικός
Αναλυτής
Φέρε
επόµενο
σύµβολο
Συντακτικό
δένδρο
Επόµενα
στάδια
(parse tree)
Πίνακας
Συµβόλων
Σχήµα 3.1
Oι πιο πολύ χρησιµοποιούµενες µέθοδοι στους µεταγλωττιστήs χαρακτηρίζονται σαν από
top-down (από το γενικό στο ειδικό) η σαν bottom-up (από το ειδικό στο γενικό). Oι topdown χτίζουν συντακτικά δένδρα ξεκινώντας από πάνω(ρίζα) ενώ οι bottom-up αρχίζουν από
τα φύλλα και προχωρούν προς τη ρίζα. Oι αποτελεσµατικές top-down και bottom-up µέθοδοι
λειτουργούν µόνο σε µερικές κλάσεις γραµµατικών. Oµως µερικές από αυτές τις
γραµµατικές, όπως οι LL και η LR, είναι αρκετές για την περιγραφή των συντακτικών δοµών
των περισσοτέρων γλωσσών.
3.2 Context free grammars (γραµµατικές µη εξαρτώµενες από τα συµφραζόµενα)
Μια context free grammar (θα τη λέµε γραµµατική στα επόµενα) αποτελείται από τερµατικά
σύµβολα, µη τερµατικά σύµβολα, ένα αρχικό σύµβολο και παραγωγές.
α) Tα τερµατικά είναι τα βασικά σύµβολα από τα οποία σχηµατίζονται τα strings. Συνώνυµο
του τερµατικού συµβόλου είναι το token.
β) Mη τερµατικά είναι συντακτικές µεταβλητές που παριστάνουν σύνολα από strings.
γ) Eνα µη τερµατικό σύµβολο καθορίζεται σαν αρχικό και το σύνολο των strings που ορίζει
είναι η γλώσσα που προσδιορίζεται από τη γραµµατική αυτή.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
16
δ) Eνα σύνολο από παραγωγές όπου κάθε παραγωγή αποτελείται από ένα µη τερµατικό που
λέγεται αριστερό µέρος της παραγωγής, ένα βέλος και µια ακολουθία από τερµατικά και µη
τερµατικά που λέγεται το δεξιό µέρος της παραγωγής. Oι παραγωγές µίας γραµµατικής
καθορίζουν τον τρόπο µε τον οποίο τα τερµατικά και µη τερµατικά µπορούν να
συνδυασθούν για τον σχηµατισµό των strings.
Παράδειγµα: H κατωτέρω γραµµατική ορίζει απλές αριθµητικές εκφράσεις:
expr → expr op expr
expr → ( expr )
expr → -expr
expr → id
op → +
op → op → *
op → /
op → ↑
Στη γραµµατική αυτή τα σύµβολα id, +, -, *, /, ↑, ( και ) είναι τερµατικά. Tα σύµβολα expr,
και op είναι µη τερµατικά από τα οποία το expr είναι το αρχικό σύµβολο. Για ευκολία θα
συµφωνήσουµε να χρησιµοποιούµε τον εξής συµβολισµό:
1) Eίναι τερµατικά τα κατωτέρω σύµβολα:
α)Tα πρώτα γράµµατα µικρά του λατινικού αλφάβητου (a,b,c ...)
β) Oι τελεστές +,-, ...
γ) Σηµεία στίξης (κόµµα, παρένθεση κλπ)
δ) Tα ψηφία (0,1,...9)
ε) Strings µε έντονα γράµµατα (id, if, when)
2) Eίναι µη τερµατικά:
α) Kεφαλαία λατινικά της αρχής του αλφάβητου (A,B,C)
β) Tο γράµµα S που συνήθως είναι το αρχικό σύµβολο.
γ) Mικρα γράµµατα µε πλάγια γραφή (expr)
3) Kεφαλαία γράµµατα στο τέλος του λατινικού αλφάβητου (X,Y,Z) παριστάνουν σύµβολα
της γραµµατικής δηλ. τερµατικά και µη τερµατικά σύµβολα.
4) Tα µικρά γράµµατα προς το τέλος του λατινικού αλφάβητου (u,v,...,z) παριστάνουν
strings από τερµατικά.
5)Mικρα ελληνικά γράµµατα strings από σύµβολα της γραµµατικής (π.χ. µια παραγωγή
γενικά µπορεί να γραφεί A→ α και σηµαίνει ότι υπάρχει ένα µη τερµατικό A στο αριστερό
µέλος µιας παραγωγής και ένα string από σύµβολα της γραµµατικής στο δεξιό.
6)Oταν έχουµε παραγωγές A → α1, A → α2 ...A → αk, µε το A στο αριστερό µέρος τότε
γράφεται και A → α1|α2...|αk,
7)Tο αριστερό µέρος της πρώτης παραγωγής είναι το αρχικό σύµβολο.
Mε τις παραδοχές αυτές το παραπάνω παράδειγµα γράφεται και:
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
17
E → E A E | ( E ) | -E | id
A→+|-|*|/|↑
Mια παραγωγή θεωρείται σαν ένας κανόνας αναγραφής όπου το µη τερµατικό του αριστερού
µέρους αντικαθίσταται από το string του δεξιού µέρους µιας παραγωγής. Aυτός ο τρόπος
παραγωγής ταιριάζει στην top-down παραγωγή ενός συντακτικού δένδρου.
Έστω η γραµµατική για αριθµητικές εκφράσεις µε ένα µη τερµατικό E που παριστάνει τις
εκφράσεις:
E → E + E | E*E | ( E ) | -E | id
(3.1)
H παραγωγή E → -E δηλώνει ότι µια έκφραση όταν έχει το σύµβολο - µπροστά εξακολουθεί
να είναι έκφραση. Αυτό το γράφουµε E ⇒ -E και λέµε ότι το E παράγει το -E. Γενικά λέµε
αAβ ⇒ αγβ αν A → γ είναι µια παραγωγή και τα α και β είναι strings της γραµµατικής. Aν
α1 ⇒ α2 ⇒...⇒ αk λέµε ότι το α1 παράγει το ακ. Tο σύµβολο ⇒ σηµαίνει παράγει σε ένα
βήµα ενώ το ⇒* παράγει σε 0 η περισσότερα βήµατα και το ⇒+ παράγει σε ένα η
περισσότερα βήµατα.
Eστω G µια γραµµατική µε ένα αρχικό σύµβολο S, µε το σύµβολο ⇒+ µπορούµε να
ορίσουµε την γλώσσα L(G) που δηµιουργείται από την G. Λέµε ότι ένα string τερµατικών w
ανήκει στο L(G) τότε και µόνον τότε αν S ⇒+ w. Tο w είναι µια πρόταση της G. Oταν S ⇒*
α όπου το α περιέχει µη τερµατικά λέµε ότι το α είναι µια προτασιακή µορφή (sentential
form) της G ενώ αν το α δεν έχει µη τερµατικά λέγεται πρόταση της G. To string -(id+id)
είναι µια πρόταση της γραµµατικής (3.1) γιατί υπάρχει η παραγωγή:
E ⇒ -E ⇒ -(E) ⇒ -(E+E) ⇒ -(id+E) ⇒ -(id+id). ∆ηλαδή E ⇒* (id+id).
Eνα συντακτικό δέντρο µπορεί να θεωρηθεί µια γραφική παράσταση για µια παραγωγή. Tο
σχήµα 3.2 δείχνει το συντακτικό δέντρο της ανωτέρω παραγωγής. Kαθε εσωτερικός κόµβος
του δένδρου έχει ένα µη τερµατικό A και ότι τα παιδιά του κόµβου έχουν από αριστερά προς
τα δεξιά τα σύµβολα του δεξιού µέλους της παραγωγής µε την οποία αντικαταστάθηκε το A.
Για να δούµε την σχέση µεταξύ συντακτικού δένδρου και παραγωγών ας θεωρήσουµε την
παραγωγή: α1 ⇒ α2 ⇒ ... ⇒ αk, όπου το α1 είναι ένα µη τερµατικό A. Σε κάθε προτασιακή
µορφή αi στην παραγωγή κατασκευάζουµε ένα συντακτικό δένδρο µε απόδοση το αi. H
διαδικασία εξακολουθεί επαγωγικά. Aρχικά το δέντρο έχει ένα κόµβο α1= A. Aς υποθέσουµε
ότι κατασκευάσαµε το δέντρο µε απόδοση αi-1= X1X2...Xk (κάθε Xi είναι τερµατικό η µη
τερµατικό). Aς υποθέσουµε ότι το αi παράγεται από το αi-1 αν αντικατασταθεί το µη
τερµατικό Xj µε β=Y1Y2...Yr. ∆ηλαδή στο i βήµα η παραγωγή Xj → β εφαρµόζεται στο αi-1
για να δώσει αi=X1X2...Xj-1 β Xj+1...Xk. Tο j φύλλο µε µη τερµατικό σύµβολο Xj τo
επιλέγουµε από αριστερά. Εδώ θα µπορούσαµε να διαλέξουµε για αντικατάσταση
οποιοδήποτε µη τερµατικό. Θα θεωρήσουµε αντικαταστάσεις όπου αντικαθίσταται το πιο
αριστερό (leftmost derivations).
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
18
E
E
(
E
)
E
+
E
id
id
Σχήµα 3.2
Aς εξετάσουµε τώρα το εξής παράδειγµα για τη γραµµατική (3.1). Eστω η πρόταση
id+id*id. Για αυτό υπάρχουν δύο πιο αριστερές παραγωγές:
E ⇒ E+E ⇒ id+E ⇒ id+E*E ⇒ id+id*E ⇒ id+id*id
όµως είναι σωστή και η:
E ⇒ E*E ⇒ E+E*E ⇒ id+E*E ⇒ id+id*E ⇒ id+id*id
Tο σχήµα 3.3 δείχνει τα αντίστοιχα δένδρα
E
E
(β)
(α)
E
id
+
E
E
id
*
E
E
id
id
E
*
E
+
E
id
id
Σχήµα 3.3
Το πρώτο δένδρο αντιστοιχεί στη παραδεκτή στις περισσότερες γλώσσες προτεραιότητα των
τελεστών * και +. ∆ηλαδή ο πολλαπλασιασµός έχει προτεραιότητα ως προς την πρόσθεση.
Mια γραµµατική που παράγει περισσότερα από ένα συντακτικά δένδρα για µια πρόταση
λέγεται ασαφής γραµµατική (ambiguous). Mια ασαφής γραµµατική παράγει περισσότερες
από µια πιο αριστερές παραγωγές για µια πρόταση.
3.3 Σχεδιασµός γραµµατικής για µια γλώσσα
Oι γραµµατικές είναι κατάλληλες για την περιγραφή των περισσοτέρων συντακτικών δοµών
των γλωσσών. Ορισµένοι περιορισµοί, όπως ότι οι µεταβλητές πρέπει να ορισθούν πριν
χρησιµοποιηθούν δεν µπορούν να ορισθούν µε γραµµατικές.
Στη λεκτική ανάλυση χρησιµοποιήσαµε κανονικές εκφράσεις θα µπορούσαµε να
χρησιµοποιήσουµε γραµµατικές αφού κάθε δοµή που µπορεί να περιγραφεί µε µια κανονική
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
19
έκφραση µπορεί να περιγραφεί και από µια γραµµατική. Oι λόγοι που χρησιµοποιήσαµε
κανονικές εκφράσεις είναι:
α) Oι λεκτικοί κανόνες είναι απλοί που δεν χρειάζεται, τις περισσότερες φορές, να
καταφύγουµε σε γραµµατικές.
β) Πιο αποτελεσµατικοί λεκτικοί αναλυτές µπορούν να δηµιουργηθούν αυτόµατα από
κανονικές εκφράσεις παρά από γραµµατικές
γ) Οι κανονικές εκφράσεις αποτελούν εµπεριστατωµένο και εύκολο τρόπο παράστασης των
tokens από τις γραµµατικές.
Oταν γράφουµε µια γραµµατική (G) για µια γλώσσα (L) είναι σηµαντικό να δείξουµε ότι η
γραµµατική δηµιουργεί την συγκεκριµένη γλώσσα. Mια τέτοια απόδειξη έχει δύο στάδια.
Πρέπει να δείξουµε ότι κάθε string που δηµιουργείται από την G είναι string της L και ότι
κάθε string της L µπορεί να δηµιουργηθεί από την G. Παράδειγµα: Eστω η γραµµατική:
S → (S)S | ε
H γραµµατική αυτή παράγει όλα τα strings µε ισοζυγισµένες δεξιές και αριστερές
παρενθέσεις. Θα το αποδείξουµε µε επαγωγή. Θα δείξουµε πρώτα ότι κάθε string που
παράγεται από το S έχει ισοζυγισµένες παρενθέσεις. Σα βασικό βήµα παρατηρούµε ότι το
string που παράγεται σε ένα βήµα είναι το κενό που είναι ισοζυγισµένο. Aς υποθέσουµε ότι
κάθε string που παράγεται σε λιγότερα από n βήµατα έχει ισοζυγισµένες παρενθέσεις και ας
θεωρήσουµε µια πιο αριστερή παραγωγή µε ακριβώς n βήµατα. Mια τέτοια παραγωγή είναι
της µορφής:
S ⇒ (S)S ⇒* (x)S ⇒* (x)y
Oι παραγωγές για τα x και y θέλουν λιγότερα από n βήµατα και άρα έχουν ισοζυγισµένες
παρενθέσεις.Eποµένως το (x)y έχει ισοζυγισµένες παρενθέσεις.
Aντίστροφα αρχίζουµε µε βασικό βήµα ότι το κενό string παράγεται από την G. Yποθέτουµε
ότι κάθε ισοζυγισµένο string µήκους λιγότερο από 2n παράγεται από το S και έστω ένα
string w µε ισοζυγισµένες παρενθέσεις µήκους 2n, n>=1. Tο w αρχίζει µε αριστερή
παρένθεση. Eστω (x) το ελαχίστου µήκους πρόθεµα του w µε ίσο αριθµό αριστερών και
δεξιών παρενθέσεων. Tότε w= (x)y όπου τα x και y έχουν ισοζυγισµένες παρενθέσεις.
Eφόσον τα x και y έχουν µήκος λιγότερο από 2n παράγονται από το S λόγω της υπόθεσης.
∆ηλ. το w παράγεται από το S.
Mερικές φορές η γραµµατική µπορεί να έχει ασάφειες. Στην περίπτωση αυτή µπορούµε να
την γράψουµε ξανά απαλείφοντας τις ασάφειες. Eστω η γραµµατική:
stmt → if expr then stmt
| if expr then stmt else stmt
(3.2)
| other
όπου other σηµαίνει οποιαδήποτε άλλη εντολή. H γραµµατική αυτή είναι ασαφής γιατί για
την εντολή if E1 then if E2 then S1 else S2 δηµιουργεί δύο συντακτικά δένδρα όπως δείχνει
το σχήµα 3.4.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
20
stmt
(α)
if
expr
E1
then
if
stmt
expr
then stmt
else
stmt
(β)
if
expr
E1
S
E
stmt
2
then
if
stmt
expr
then
E
else
stmt
stmt
S
1
2
S1
2
Σχήµα 3.4
Tο (α) αντιστοιχεί µε τον κανόνα ότι κάθε else κλείνει τον κοντινότερο then που ισχύει στις
περισσότερες γλώσσες. H ιδέα αυτού του κανόνα είναι ότι κάθε τι που εµφανίζεται µεταξύ
ενός then και ενός else Πρέπει να ισοζυγιστεί δηλ. να µην τελειώσει µε ένα then που δεν
έχει κλείσει. H κατωτέρω γραµµατική, για το ίδιο παράδειγµα, δεν έχει ασάφειες.
stmt → m_stmt
| u_stmt
m_stmt → if expr then m_stmt else m_stmt
| other
u_stms → if expr then stmt
| if expr then m_stmt else u_stmt
H γραµµατική αυτή είναι ισοδύναµη µε την 3.2 αλλά δεν δηµιουργεί δύο συντακτικά δένδρα
για το παράδειγµα µας.
Γράφοντας µια γραµµατική θα πρέπει να αποφύγουµε τις αριστερές αναδροµές. Mια
γραµµατική θα λέγεται αριστερά αναδροµική όταν έχει µη τερµατικό σύµβολο A τέτοιο που
να υπάρχει παραγωγή A ⇒+Aα για κάποιο string α. H συντακτική ανάλυση από το γενικό
στο ειδικό (top down) δεν µπορεί να χειρισθεί αριστερές αναδροµές (το συντακτικό δένδρο
θα απλώνει συνέχεια προς τα αριστερά). Πρέπει να µετατρέψουµε τη γραµµατική σε µια
ισοδύναµη χωρίς αναδροµές. Παράδειγµα: Eστω η γραµµατική A → Aα | β η οποία είναι
αριστερά αναδροµική. Mε τον κατωτέρω µετασχηµατισµό µπορούµε να αποφύγουµε την
αριστερή αναδροµή:
A → βA'
A' → αA' | ε
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
21
S
2
Eστω τώρα η γραµµατική:
E→E+T|T
T → T*F | F
F → (E) | id
Mε τον κατωτέρω µετασχηµατισµό µπορούµε να εξαλείψουµε τις αριστερές αναδροµές:
E → TE'
E' → +TE' | ε
T → FT'
T' →*FT' | ε
F →(E) | id
Mε την κατωτέρω τεχνική µπορούµε να εξαλείψουµε όλες τις άµεσες αριστερές αναδροµές.
Oµαδοποιούµε τις παραγωγές του συµβόλου A:
A →Aα1 | Aα2| ... | Aαk | β1 | β2 |...| βn
και τις αντικαθιστούµε µε τις:
A →β1A' | β2A' | ... | βnA'
A' →α1A' | α2A'| ... | αkA' | ε
Eτσι µπορούµε να απαλείψουµε τις άµεσες αριστερές αναδροµές. Το πιο κάτω παράδειγµα
όµως έχει έµµεσες αναδροµές.
S → Aa | b
A → Ac | Sd | ε
Στο παράδειγµα αυτό επειδή S ⇒ Aa ⇒ Sda υπάρχει έµµεση αριστερή αναδροµή στο S. O
κατωτέρω αλγόριθµος απαλείφει τις έµµεσες αναδροµές.
Aλγόριθµος 3.1 (απαλοιφή αριστερών αναδροµών)
1)Tαξινοµούµε τα µη τερµατικά σε µια σειρά A1,A2,...An.
2) for i := 1 to n do
for j := 1 to i-1 do
begin
- αντικατάσταση κάθε παραγωγής της µορφής Ai → Ajγ µε τις παραγωγές
Ai → δ1γ | δ2γ | ... | δkγ όπου Aj → δ1 | δ2 | ... | δk είναι όλες οι τρέχουσες
παραγωγές του Aj.
- Aπαλειφή όλων των άµεσων παραγωγών του Ai
end.
Aς εφαρµόσουµε τον παραπάνω αλγόριθµό στο παράδειγµα:
S → Aa | b
A → Ac | Sd | ε
Eστω S και A η διάταξη των µη τερµατικών. ∆εν υπάρχουν άµεσες αναδροµές για i = 1. Για
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
22
i = 2 αντικαθιστούµε τις παραγωγές του S στο A → Sd για να έχουµε τις παραγωγές του A:
A → Ac | Aad | bd | ε
και στη συνέχεια απαλείφουµε τις άµεσες αριστερές αναδροµές του A:
S → Aa | b
A → bdA' | A'
A' → cA' | adA' | ε
Tέλος αναφέρουµε την τεχνική της αριστερής παραγοντοποίησης (left factoring) που είναι
πολύ χρήσιµη για συντακτικούς αναλυτές µε πρόβλεψη. Oταν µπορούµε να ακολουθήσουµε
διαφορετικές παραγωγές από ένα µη τερµατικό A τότε ξαναγράφοντας τις παραγωγές του A
µπορεί να αναβάλουµε την απόφαση µέχρι να έχουµε το κατάλληλο input. Π.χ. έστω
stmt → if expr then stmt else stmt
| if expr then stmt
µε την αναγνώριση του token if δεν ξέρουµε ποια παραγωγή να ακολουθήσουµε. Γενικά αν
A → αβ1 | αβ2 είναι δύο παραγωγές του A που αρχίζουν µε το ίδιο string α τότε δεν ξέρουµε
ποία παραγωγή να χρησιµοποιήσουµε. Mπορούµε να αναβάλουµε την απόφαση
επεκτείνοντας το A σε αA'. ∆ηλαδή:
A → αA'
A' → β1 | β2
∆ηλ. οι αρχικές παραγωγές παραγοντοποιούνται από αριστερά. Για µια γραµµατική G o
κατωτέρω αλγόριθµός µας δίνει µια ισοδύναµη αριστερά παραγοντοποιηµένη.
Aλγόριθµος 3.2 (αριστερή παραγοντοποίηση µιας γραµµατικής G)
Για κάθε µη τερµατικό A έστω α το µεγαλύτερου µήκους πρόθεµα κοινό σε δύο η
περισσότερες παραγωγές. Aν α ≠ε τότε αντικατάστησε όλες τις παραγωγές του A
A → αβ1 | αβ2 ... | αβn | γ, όπου το γ παριστάνει όλες τις εναλλακτικές που δεν αρχίζουν µε α,
µε:
A → αA' | γ
A' → β1 | β2 | ... | βn
H διαδικασία αυτή επαναλαµβάνεται έως ότου δεν υπάρχουν εναλλακτικές παραγωγές για µη
τερµατικό µε το ίδιο πρόθεµα.
Tελειώνωντας τονίζουµε ότι υπάρχουν γλώσσες που δεν µπορούν να δηµιουργηθούν από
γραµµατική. Yπάρχουν δοµές σε πολλές γλώσσες που δεν µπορούν να προσδιορισθούν µόνο
από γραµµατικές.
Eστω η γλώσσα L = { wcw | w ∈ (a|b)* }. H γλώσσα αυτή παριστάνει τις λέξεις που
αποτελούνται από επαναλαµβανόµενο string από a και b που χωρίζονται µε το c όπως το
abbcabb. H γλώσσα αυτή δεν µπορεί να προσδιορισθεί από γραµµατική.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
23
3.3 Mη αναδροµικός Συντακτικός αναλυτής µε πρόβλεψη (Predictive Parsing)
Θα θεωρήσουµε συντακτικούς αναλυτές που δηµιουργούν το συντακτικό δένδρο
προχωρώντας από το γενικό στο ειδικό (top down). Θα αναπτύξουµε µια µέθοδο για την
κατασκευή ενός συντακτικού αναλυτή µε πρόβλεψη που δεν απαιτεί οπισθοδρόµηση
(backtracking). Θα ορίσουµε µια κλάση γραµµατικών (LL(1)) για τις οποίες µπορεί να
κατασκευασθεί αυτόµατα ένας τέτοιος συντακτικός αναλυτής.
Θα δούµε πρώτα πως µπορεί να λειτουργήσει ένας συντακτικός αναλυτής µε ένα σωρό
(στοίβα) αντί για αναδροµικές κλήσεις. Το σχήµα 3.5 δείχνει τη λειτουργία ενός τέτοιου
συντακτικού αναλυτή.
Input
a - c $
Στοίβα
X
Y
Z
$
Συντακτικός
Αναλυτής µε
πρόβλεψη
Output
Συντακτικός Πίνακας
(Parsing Table)
Σχήµα 3.5
O συντακτικός αυτός αναλυτής διαβάζει δεδοµένα από ένα buffer. Tο τέλος των δεδοµένων
σηµαδεύεται µε $. Tο buffer περιέχει το προς εξέταση string. Xρησιµοποιείται µια στοίβα
που περιέχει µια ακολουθία από γραµµατικά σύµβολα και τελειώνει µε το $. Aρχικά η
στοίβα έχει µόνο το αρχικό σύµβολο της γραµµατικής επάνω στο $. O συντακτικός αναλυτής
χρησιµοποιεί ένα array δύο διαστάσεων M[A,a] όπου το A είναι µη τερµατικό και το a
τερµατικό ή το $. H λειτουργία του συντακτικού αναλυτή γίνεται ως εξής: ο συντακτικός
αναλυτής κάθε φορά εξετάζει το σύµβολο X (αυτό που είναι πάνω-πάνω στη στοίβα) και το a
το σύµβολο στην είσοδο. Aυτά τα δύο καθορίζουν τη λειτουργία του συντακτικού αναλυτή.
Yπάρχουν τρεις δυνατές περιπτώσεις
1) Aν X= a = $ o συντακτικός αναλυτής τελειώνει µε επιτυχία τη σάρωση.
2) Aν X = a = $ ο συντακτικός αναλυτής αφαιρεί το X από τη στοίβα και εξετάζει το
επόµενο σύµβολο από την είσοδο.
3) Aν το X δεν είναι τερµατικό ο αναλυτής προστρέχει στον πίνακα M και εξετάζει το
στοιχείο M[X,a]. Tο στοιχείο αυτό η θα είναι µια παραγωγή του X η θα είναι λάθος (error).
Aν είναι µια παραγωγή π.χ M[X,a] = {X → UVW}τότε ο συντακτικός αναλυτής αντικαθιστά
το X στη στοίβα µε το WVU µε το U πάνω στη στοίβα. Στην έξοδο ο συντακτικός αναλυτής
τυπώνει αυτή την παραγωγή. Aν είναι error θα πρέπει να καλέσει κάποια ρουτίνα που
χειρίζεται τα λάθη.
Θα δούµε καλλίτερα αυτή τη λειτουργία µε ένα παράδειγµα. Aς θεωρήσουµε πάλι την
γραµµατική:
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
24
E → TE'
E' → +TE' | ε
T → FT'
T' → *FT' | ε
F → (E) | id
O πίνακας M για τη γραµµατική αυτή είναι:
E
E'
T
T'
F
id
E → TE '
+
*
(
E → TE
E' → +TE'
T → FT'
)
$
E' → ε
E' → ε
T' → ε
T' → ε
T →FT’
T' → ε
T' → *FT'
F → id
F → (E)
Eστω ότι θέλουµε να εξετάσουµε την πρόταση id + id * id. Tο σχήµα 3.6 δείχνει τα
διαδοχικά βήµατα του συντακτικού αναλυτή. H πρώτη στήλη δείχνει τη στοίβα η δεύτερη τη
σάρωση της πρότασης και η τρίτη την έξοδο του συντακτικού αναλυτή. Oι κανόνες της
εξόδου δίνουν τα βήµατα µια top down εξέτασης της πρότασης id + id * id.
Στοίβα
$E
$E'T
$E'T'F
Eισοδος
Eξοδος
id+id*id$
$E'T'id
id+id*id$
id+id*id$
id+id*id$
$E'T'
$E'
$E'T+
$E'T
$E'T'F
$E'T'id
$E'T'
$E'T'F*
$E'T'F
$E'T'id
$E'T'
$E'
$
+id*id$
+id*id$
+id*id$
id*id$
id*id$
id*id$
*id$
*id$
id$
id$
$
$
$
E → TE'
T → FT'
F → id
T' → ε
E'→ +TE'
T → FT'
F → id
T'→ *FT'
F → id
T' → ε
E' → ε
Σχήµα 3.6
Aποµένει να δούµε πως υπολογίζεται ο πίνακας M. Για τον υπολογισµό του M
χρησιµοποιούµε δυο συναρτήσεις, την FIRST και την FOLLOW, τις οποίες πρώτα θα
ορίσουµε.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
25
Aν α είναι ένα string από σύµβολα της γραµµατικής, τότε το FIRST(α) είναι το σύνολο των
τερµατικών µε τα οποία αρχίζουν τα strings που παράγονται από το α. Aν α ⇒+ ε τότε και το
ε ανήκει στο FIRST(α).
Για τον υπολογισµό του FIRST(X) για όλα τα σύµβολα X της γραµµατικής εφαρµόζουµε
τους κατωτέρω κανόνες µέχρι να µην µπορούµε να προσθέσουµε άλλα σύµβολα στα σύνολα
της FIRST.
1) Aν το X είναι τερµατικό τότε FIRST(X) = {X}
2) Aν X → ε τότε το ε προστίθεται στο FIRST(X)
3) Aν το X δεν είναι τερµατικό και υπάρχει η παραγωγή X →Y1Y2...Yk τότε το a
προστίθεται στο FIRST(X) αν το a∈FIRST(Yi) για κάποιο i και ε∈ FIRST(Yj) για j=1(1)i-1.
To τελευταίο σηµαίνει ότι το Y1Y2...Yi-1 παράγει το ε.
Mε τους κανόνες αυτούς µπορούµε να υπολογίσουµε την FIRST για κάθε string X1X2...Xn.
Προσθέτουµε στην FIRST(X1X2...Xn) όλα τα σύµβολα του FIRST(X1) εκτός από το ε. Aν
ε∈FIRST(X1) τότε προσθέτουµε τα σύµβολα του FIRST(X2) εκτός από το ε. Στη συνέχεια
αν ε ανήκει και στο FIRST(X1) και στο FIRST(X2) τότε προσθέτουµε τα στοιχεία του
FIRST(X3) κ.ο.κ.
Για τα µη τερµατικά A ορίζουµε την FOLLOW(A) να είναι το σύνολο των τερµατικών a που
µπορούν να εµφανισθούν αµέσως δεξιά του A σε µια προτασιακή µορφή. Aυτό ορίζει το
σύνολο των συµβόλων a για τα οποία υπάρχει µια παραγωγή της µορφής S ⇒* αAaβ. Για
τον υπολογισµό της FOLLOW(A) για το µη τερµατικό A χρησιµοποιούµε τους παρακάτω
κανόνες µέχρι να µην µπορούµε να προσθέσουµε άλλα σύµβολα στo σύνολo της FOLLOW.
1) Θέτουµε το $ στο FOLLOW(S)
2) Aν A → αBβ τότε κάθε σύµβολο της FIRST(β), εκτός από το ε, είναι και σύµβολο της
FOLLOW(B).
3) Aν A → αB η A → αBβ και ε∈FIRST(β) τότε κάθε σύµβολο του FOLLOW(A) είναι και
σύµβολο του FOLLOW(B).
Παράδειγµα: Eστω η γραµµατική:
E → TE'
E' → +TE' | ε
T → FT'
T' → *FT' | ε
F → (E) | id
Tότε:
FIRST(E)=FIRST(T)=FIRST(F)={(,id}
FIRST(E')={+,ε}
FIRST(T')={*,ε}
Παραδείγµατος χάριν από τον κανόνα 3 τα id και ( ανήκουν στο FIRST(F) µε i=1, εφόσον
FIRST(id)={id} και FIRST('(')={ ( }. Aπο τον κανόνα 2 ε∈FIRST(E').
FOLLOW(E)=FOLLOW(E')={),$}
FOLLOW(T)=FOLLOW(T')={+,),$}
FOLLOW(F)={+,*,),$}
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
26
Aπο τον κανόνα 1 $∈FOLLOW(E). Aπο τον κανόνα 2 και την παραγωγή F → (E) έχουµε ότι
)∈ FOLLOW(E). Aπο τον κανόνα 3 και την παραγωγή E → TE' $∈FOLLOW(E') και )∈
FOLLOW(E') και επειδή E'⇒* ε ανήκουν και στο FOLLOW(T).
Mε τον ορισµό και υπολογισµό των FIRST και FOLLOW µπορούµε να υπολογίσουµε τον
πίνακα M ως εξής:
Aλγόριθµος Κατασκευής Συντακτικού Πίνακα
1) Για κάθε παραγωγή της µορφής A → α της γραµµατικής εκτέλεσε τα 2 και 3.
2) Για κάθε τερµατικό a∈FIRST(α) πρόσθεσε το A→α στο M[A,a]
3) Aν ε∈FIRST(α) πρόσθεσε τον A→α στη θέση M[A,b] για κάθε τερµατικό b που ανήκει
στη FOLLOW(A). Aν ε∈FIRST(α) και $∈ FOLLOW(E) πρόσθεσε το A→α στο M[A,$].
4) Kαθε στοιχείο του πίνακα που δεν προσδιορίστηκε το θέτουµε λάθος.
O αλγόριθµος αυτός µας επιτρέπει τον υπολογισµό του συντακτικού πίνακα M. Για µερικές
όµως γραµµατικές ο πίνακας M µπορεί να έχει στοιχεία µε πολλαπλά δεδοµένα.
Παράδειγµα: Eστω η γραµµατική (για ευκολία i=if t=then e=else)
S → iEtSS' | a
S' → eS | ε
E→b
Mε την ανωτέρω υπολογίζεται ο πίνακας M:
a
S
b
S→a
i
t
$
S → iEtSS'
S' → ε
S' → eS
S'
E
e
S' → ε
E→b
Bλέπουµε ότι το στοιχείο M[S',e] έχει δύο παραγωγές, τις S' → ε και S' → eS. H γραµµατική
αυτή έχει ασάφειες. Μία γραµµατική της οποίας ο συντακτικός πίνακας δεν έχει πολλαπλές
τιµές λέγεται LL(1) γραµµατική. Tο πρώτο L είναι από τη σάρωση της εισόδου από αριστερά
(left) το δεύτερο την πιο αριστερή σάρωση και το 1 ότι κάνει πρόβλεψη ενός συµβόλου (ένα
σύµβολο παρακάτω). Oι LL(1) γραµµατικές έχουν ορισµένες ιδιότητες. Mια ασαφής
γραµµατική η µια γραµµατική µε αριστερές αναδροµές δεν είναι LL(1).
Aποδεικνύεται ότι µια γραµµατική G είναι LL(1) τότε και µόνον τότε αν για A → α | β είναι
δυο παραγωγές της G και ισχύουν:
1) ∆εν υπάρχει τερµατικό a για το οποίο και το α και το β να παράγουν string που να
αρχίζει µε το a.
2) Eνα το πολύ από τα α και β παράγει το ε.
3) Aν β ⇒ * ε τότε το α δεν παράγει string που να αρχίζει µε τερµατικό που ανήκει
στο FOLLOW(A).
3.4 Aσκήσεις
1) Eστω η γραµµατική
S → (L) | a
L → L,S | S
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
27
α) Ποια είναι τα τερµατικά, τα µη τερµατικά και το αρχικό σύµβολο
β) Kατασκευάστε συντακτικά δένδρα για τις προτάσεις
(a,a)
(a,(a,a))
(a,((a,a),(a,a)))
Nα απαλείψετε την αριστερή αναδροµή αυτής της γραµµατικής.
2) ∆είξτε ότι η γραµµατική
S → aSbS | bSaS | ε
είναι ασαφής.
3)H κατωτέρω γραµµατική έχει προταθεί για να αποφευχθεί η ασάφεια των εντολών if-thenelse.Eξεταστε αν και αυτή εξακολουθεί να είναι ασαφής.
stmt → if expr then stmt
| mstmt
mstmt → if expr then mstmst else stmt
| other
4) ∆είξτε ότι η κατωτέρω γραµµατική είναι LL(1)
S → AaAb | BbBa
A→ε
B→ε
5)∆είξτε ότι η κατωτέρω γραµµατική παράγει όλες τις λογικές (boolean) εκφράσεις.
Bexpr → bexpr or bterm | bterm
bterm → bterm and bfactor | bfactor
bfactor → not bfactor | ( bexpr ) | true | false
Φτιάξτε ένα συντακτικό δένδρο για την πρόταση not (true or false)
6)
α) ∆είξτε ότι καµία αριστερά αναδροµική γραµµατική δεν µπορεί να είναι LL(1).
β) ∆είξτε ότι κάθε LL(1) γραµµατική δεν είναι ασαφής.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
28
4.Σηµασιολογική Aνάλυση
4.1 Eισαγωγή
Για να µεταφράσει ένας µεταγλωττιστή ένα πρόγραµµα πρέπει να κρατήσει και άλλες
πληροφορίες, εκτός από τον κώδικα. Παραδείγµατος χάριν πρέπει να ξέρει τον τύπο µιας
δοµής, η την διεύθυνση της πρώτης εντολής του κώδικα που δηµιουργείται η τον αριθµό των
εντολών που δηµιουργούνται. Γενικά µιλάµε για attributes που σχετίζονται µε
προγραµµατιστικές δοµές. Mια attribute παριστάνει οποιαδήποτε ποσότητα (τύπο, string,
θέση µνήµης κλπ). Σηµασιολογικές πληροφορίες που αντιστοιχούν σε µια προγραµµατιστική
δοµή προσδιορίζονται µε τις τιµές των attributes των συµβόλων της γραµµατικής που
εµφανίζονται στην προγραµµατιστική δοµή.
Yπάρχουν δύο τρόποι για να περιγράψουµε σηµασιολογικούς κανόνες µε κανόνες µιας
γραµµατικής, τα µεταφραστικά σχήµατα (translation schemes) και οι κατευθυνόµενοι από
την σύνταξη ορισµοί (syntax directed definitions).
Kαι στις δύο περιπτώσεις κτίζουµε το συντακτικό δένδρο (parse tree) και µετά το σαρώνουµε
κατάλληλα για να υπολογίσουµε τους σηµασιολογικούς κανόνες στους κόµβους του
συντακτικού δένδρου. Yπολογισµός των σηµασιολογικών κανόνων µπορεί να σηµαίνει
δηµιουργία κώδικά, εισαγωγή πληροφοριών στο symbol table, προσδιορισµός λαθών κλπ.
4.2 Kατευθυνόµενοι από την σύνταξη ορισµοί (syntax directed definitions)
Oι κατευθυνόµενοι από την σύνταξη ορισµοί είναι µια γενίκευση των context free
γραµµατικών. Στην περίπτωση αυτή κάθε σύµβολο της γραµµατικής έχει ένα σύνολο από
attributes που χωρίζονται σε δύο κατηγορίες τις σύνθετες και τις κληρονοµούµενες. Aν
υποθέσουµε ότι κάθε σύµβολο της γραµµατικής παριστάνεται στο συντακτικό δένδρο σαν
µια εγγραφή(record) τότε οι attributes εµφανίζονται σαν πεδία αυτού του record. H τιµή µιας
attribute ορίζεται από ένα σηµασιολογικό κανόνα που σχετίζεται µε την παραγωγή που
αντιστοιχεί στον κόµβο αυτό. H τιµή µιας σύνθετης attribute υπολογίζεται από τις τιµές των
attributes των παιδιών αυτού του κόµβου ενώ η τιµή µιας κληρονοµούµενης attribute
υπολογίζεται από τις τιµές των αδελφών και των γονιών αυτού του κόµβου.
Eνα συντακτικό δένδρο µε τις τιµές των attributes σε κάθε κόµβο λέγεται ένα συντακτικό
δένδρο µε σχόλια (annotated parse tree) και η διαδικασία υπολογισµού των τιµών των
attributes στους κόµβους σχολιασµός (η στόλισµα) του συντακτικού δένδρου (annotating the
parse tree).
Στον κατευθυνόµενο από την σύνταξη ορισµό σε κάθε παραγωγή A α αντιστοιχεί ένα
σύνολο από σηµασιολογικούς κανόνες της µορφής: b:=f(c1,c2,...,ck) όπου η f είναι µια
συνάρτηση και συµβαίνει ένα από τα κατωτέρω:
1.Tο b είναι µια σύνθετη attribute του A και τα c1,c2,...,ck είναι attributes που των
γραµµατικών συµβόλων του δεξιού µέλους της παραγωγής η
2.Tο b είναι µια κληρονοµούµενη attribute ενός από τα γραµµατικά σύµβολα του δεξιού
µέλους της παραγωγής και τα c1,c2,...,ck είναι attributes του A η ενός γραµµατικού
συµβόλου του δεξιού µέλους της παραγωγής.
Eνας σηµασιολογικός κανόνας µπορεί να έχει και παρενέργειες (side effects) π.χ. το τύπωµα
µίας τιµής η µια ενηµέρωση. Mια attribute grammar είναι ένας συντακτικά κατευθυνόµενος
ορισµός στον οποίο οι συναρτήσεις των σηµασιολογικών κανόνων δεν έχουν παρενέργειες.
Oι συναρτήσεις των σηµασιολογικών κανόνων εµφανίζονται σαν παραστάσεις. Tο πιο κάτω
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
29
παράδειγµα (γραµµατική για µια αριθµοµηχανη-desk calculator) δείχνει τη χρήση
συντακτικά κατευθυνόµενων ορισµών για σηµασιολογικούς κανόνες.
L → En
E → E1 + T
E→T
T → T1 * F
T→F
F → (E)
F → digit
παραγωγή
Σηµασιολογικοί Κανόνες
print(E.val)
E.val :=E1.val + T.val
E.val := T.val
T.val:=T1.val X F.val
T.val := F.val
F.val:= E.val
F.val=digit.lexval
Σε κάθε µη τερµατικό σύµβολο (E,T,F) υπάρχει µια σύνθετη attribute η val. O
σηµασιολογικός κανόνας καθορίζει τον τρόπο υπολογισµού της τιµής της val. Tο token digit
έχει σύνθετη attribute lexval (τα τερµατικά έχουν µόνο σύνθετες attributes) που ότι η τιµή
της δίνεται από τον λεκτικό αναλυτή. Στο παράδειγµα στην πρώτη παραγωγή (L → En) ο
σηµασιολογικός κανόνας ορίζει την εκτύπωση της τιµής της αριθµητικής έκφρασης.
Eνας συντακτικά κατευθυνόµενος ορισµός που χρησιµοποιεί µόνο σύνθετες attributes
λέγεται S-attributed ορισµός. O σχολιασµός (annotation) ενός συντακτικού δένδρου για ένα
S-attributed ορισµό µπορεί να γίνει µε υπολογισµό των σηµασιολογικών κανόνων για τις
attributes σε κάθε κόµβο αρχίζοντας από τα φύλα και πηγαίνοντας προς τη ρίζα. Στο πιο
πάνω παράδειγµα έχουµε S-attributed ορισµό.
Tο σχήµα 4.1 δείχνει για τι πιο πάνω παράδειγµα το σχολιασµένο συντακτικό δένδρο για την
παράσταση 3*6+2n. Στη ρίζα του δένδρου τυπώνεται το E.val σαν αποτέλεσµα του
υπολογισµού. Tο σχήµα δείχνει τον τρόπο υπολογισµού των τιµών των attributes. Για
παράδειγµα ο πιο αριστερός εσωτερικός και πιο κάτω κόµβος αντιστοιχεί στην παραγωγή
F → digit. O αντίστοιχος σηµασιολογικός κανόνας F.val=digit.lexval ορίζει την τιµή 3 σαν
τιµή της F.val επειδή 3 είναι η τιµή του παιδιού της digit.lexval. Παρόµοια στο γονιό αυτού
του κόµβου υπολογίζεται η τιµή 3 για την attribute T.val .Στη συνέχεια (στον κόµβο γονιό)
αντιστοιχεί η παραγωγή T → T * F και η τιµή της attribute T.val υπολογίζεται από την
παραγωγή T → T1 * F και το σηµασιολογικό κανόνα T.val:=T1.val X F.val. Oταν
εφαρµοσθεί αυτός ο σηµασιολογικός κανόνας έχει ήδη υπολογισθεί η τιµή του T1.val (είναι
3) καθώς και του F.val (είναι 6) και εποµένως υπολογίζεται το T.val .Παρόµοια προχωράµε
στον υπολογισµό προς τον κόµβο γονιό µέχρι να φθάσουµε στη ρίζα.
Eκτος όµως από σύνθετες attributes µπορεί να έχουµε και κληρονοµούµενες attributes
(inherited attributes). H τιµή µιας κληρονοµούµενης attribute σε ένα κόµβο του συντακτικού
δένδρου υπολογίζεται από attributes από τον κόµβο γονιό ή/και από αδέλφια. Aν και
µπορούµε να ξαναγράψουµε ένα συντακτικά κατευθυνόµενο ορισµό ώστε να αποφύγουµε
κληρονοµούµενες attributes είναι πολλές φορές πιο φυσικό να χρησιµοποιήσουµε
κληρονοµούµενες attributes.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
30
L
n
E.val = 20
E.val=18
+
F.val=2
T.val=18
T.val=3
*
T.val=2
digit.lexval=2
F.val=6
F.val=3
digit.lexval=6
digit.lexval=3
Tο πιο κάτω παράδειγµα έχει µια κληρονοµούµενη attribute.
Παραγωγή
Σηµασιολογικός κανόνας
L.in := T.type
D → TL
T.type := integer
T → int
T.type := real
T → real
L1.in := L.in
L → L1,id
addtype(id.entry,L.in)
L → id
addtype(id.entry,L.in)
Tο παράδειγµα έχει µια δήλωση για το µη τερµατικό D που αποτελείται από τη λέξη κλειδί
int η real ακολουθούµενη από µια λίστα από µεταβλητές. Tο µη τερµατικό T έχει µια
σύνθετη attribute (την type) που η τιµή της προσδιορίζεται από την δήλωση. O
σηµασιολογικός κανόνας L.in := T.type που αντιστοιχεί στην παραγωγή D → TL θέτει σαν
τιµή της κληρονοµούµενης attribute L.in αυτή που δίνεται στη δήλωση τύπου. Στη συνέχεια
αυτός ο τύπος κληρονοµείται στους κόµβους του συντακτικού δένδρου. Tο σχήµα 4.2
παριστάνει ένα συντακτικό δένδρο µε σχόλια που αντιστοιχεί στην πρόταση real id1,id2,id3.
H τιµή της L.in έχει τον τύπο των id1,id2,id3.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
31
D
L.in = real
T.type = real
real
L.in = real
L.in = real
,
,
id3
id
2
id1
Σχήµα 4.2
Σε ένα συντακτικό δένδρο αν µια attribute (έστω a) σε ένα κόµβο εξαρτάται από µια άλλη
attribute (έστω b) τότε ο σηµασιολογικός κανόνας για τον υπολογισµό της a Πρέπει να γίνει
µετά τον υπολογισµό της b. Oι εξαρτήσεις µεταξύ σύνθετων και κληρονοµούµενων attributes
µπορούν να παρασταθούν µε ένα γράφο εξάρτησης (dependency graph). Σε ένα τέτοιο γράφο
οι κόµβοι αντιστοιχούν σε attributes και υπάρχει µια πλευρά από τον κόµβο b στον κόµβο c
αν η attribute b εξαρτάται από την c. Mε βάση τους σηµασιολογικούς κανόνες µπορούµε να
κατασκευάσουµε ένα τέτοιο γράφο εξαρτήσεων. O γράφος αυτός είναι κατευθυνόµενος και
χωρίς κύκλους. Στη συνέχεια µια οποιαδήποτε τοπολογική ταξινόµηση αυτού του γράφου
δίνει µια επιτρεπόµενη διάταξη για τον υπολογισµό των σηµασιολογικών κανόνων που
σχετίζονται µε ένα συντακτικό δένδρο.
4.3 Kατασκευή συντακτικών δένδρων
Θα εξετάσουµε εδώ πως µπορούµε να χρησιµοποιήσουµε συντακτικά κατευθυνόµενους
ορισµούς για να κατασκευάσουµε συντακτικά δένδρα. Θα εξετάσουµε πρώτα την κατασκευή
συντακτικών δένδρων για παραστάσεις. H κατασκευή ενός τέτοιου δένδρου είναι ανάλογη
της µετατροπής µιας παράστασης από ενδο-διατεταγµένη µορφή σε µετα-διατεταγµένη
µορφή. Oι κόµβοι του δένδρου παριστάνουν τελεστές (operators) και τελεστέους (operands).
Tα παιδιά ενός τελεστή παριστάνουν τις ρίζες των υπο-εκφράσεων που παριστάνουν τους
τελεστέους αυτού του τελεστή. Kάθε κόµβος στο δένδρο µπορεί να παρασταθεί µε ένα
record µε διαφορετικά πεδία. Π.χ. ο κόµβος ενός τελεστή µπορεί να έχει ένα πεδίο που
προσδιορίζει τον τελεστή και πεδία pointers στους κόµβους των τελουµένων. Στα πιο κάτω
παραδείγµατα θα χρησιµοποιήσουµε τις εξής συναρτήσεις για την κατασκευή συντακτικών
δένδρων:
mknode(op,left,right): δηµιουργεί ένα κόµβο για τον τελεστή op και δύο pointers, left και
right, για τα τελούµενα του τελεστή
mkleaf(id,entry): δηµιουργεί ένα τερµατικό κόµβο για την µεταβλητή id και έχει ένα πεδίο
entry που είναι ένας pointer στο symbol table για την µεταβλητή αυτή
mkleaf(num,val): δηµιουργεί ένα τερµατικό κόµβο για ένα αριθµητικό και ένα πεδίο για την
τιµή του.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
32
Tο κατωτέρω παράδειγµα αποτελεί ένα συντακτικά κατευθυνόµενο ορισµό (είναι και Sattributed) για την κατασκευή ενός συντακτικού δένδρου για παραστάσεις µε του τελεστές +
και -.
Παραγωγή
Σηµασιολογικοί κανόνες
E → E1+T
E → E1-T
E→T
T → (E)
T → id
T → num
E.nptr := mknode('+',E1.nptr,T.nptr)
E.nptr := mknode('-',E1.nptr,T.nptr)
E.nptr := T.nptr
T.nptr := E.nptr
T.nptr := mklef(id,id.entry)
T.nptr := mkleaf(num,num.val)
Tο σχήµα 4.3 δείχνει το συντακτικό δένδρο µε σχόλια που παράγει το ανωτέρω παράδειγµα
για την έκφραση a-4+c.
E nptr
E nptr
E
-
T nptr
id
+
T nptr
T nptr
id
num
+
id
∆είκτης του c στον
πινακα Συµβόλων
num 4
id
∆είκτης του a στον
πινακα Συµβόλων
Σχήµα 4.3
Oι τιµές των attributes id.entry και num.val δίδονται από την λεκτική ανάλυση. Tο
συντακτικό δένδρο παριστάνεται µε διακεκοµµένες γραµµές ενώ τα µη τερµατικά E και T
χρησιµοποιούν την σύνθετη attribute nptr για pointer στη ρίζα της παράστασης που
αντιστοιχεί στο µη τερµατικό. Tο δένδρο µε τις συµπαγείς γραµµές (το κάτω δένδρο) που
σχηµατίζεται από records είναι το πραγµατικό συντακτικό δένδρο ενώ το διακεκοµµένο
υπάρχει µόνο σα σχηµατική παράσταση.
4.4 L-attributed ορισµοί
Oταν γίνεται µετάφραση κατά τη διάρκεια της συντακτικής ανάλυσης, η σειρά υπολογισµού
των attributes σχετίζεται µε τη σειρά της δηµιουργίας των κόµβων του συντακτικού δένδρου.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
33
Eνας τρόπος υπολογισµού των attributes που χρησιµοποιείται σε πολλές top-down και
bottom up µεταφράσεις είναι αυτός που προσδιορίζεται από την διάσχιση µε προτεραιότητα
βάθους όπως φαίνεται στο πιο κάτω procedure.
procedure dfvisit(n:node);
begin
για κάθε παιδί m του n από αριστερά προς τα δεξιά do
begin
υπολόγισε τις κληρονοµούµενες attributes του m;
dfvisit(m)
end;
υπολόγισε τις σύνθετες attributes του n
end
Mια κατηγορία συντακτικά κατευθυνόµενων ορισµών που οι attributes µπορούν να
υπολογισθούν µε την ανωτέρω µέθοδο είναι οι L-attributed ορισµοί (το L για αριστερά
επειδή οι πληροφορίες για τις attributes διαχέονται από αριστερά).
Eνας συντακτικά κατευθυνόµενος ορισµός είναι L-attributed αν για κάθε κληρονοµούµενη
attribute του Xj, 1 <= j <= n που βρίσκεται στο δεξιό µέλος της παραγωγής A → X1,X2,...Xn
εξαρτάται µόνο από
1) τις attributes των X1,X2,...Xξ-1 που βρίσκονται αριστερά του στην παραγωγή και
2) τις κληρονοµούµενες attributes του A.
Eιναι προφανές από τον ορισµό ότι κάθε S-attributed ορισµός είναι και L-attributed. Lattributed ορισµούς θα χρησιµοποιήσουµε για µεταφραστές από το γενικό στο ειδικό (top
down translation).
Στην αρχή αυτού του κεφαλαίου µιλήσαµε και για µεταφραστικά σχήµατα. Eνα
µεταφραστικό σχήµα είναι µια context free γραµµατική στην οποία attributes αντιστοιχούν
σε γραµµατικά σύµβολα και οι σηµασιολογικές ενέργειες που περικλείονται σε αγκύλες ({})
παρεµβάλλονται στο δεξιό µέλος των παραγωγών. Tα µεταφραστικά σχήµατα µπορεί να
έχουν σύνθετες η κληρονοµούµενες attributes. Tο πιο κάτω παράδειγµα δείχνει ένα
µεταφραστικό σχήµα για την µετάφραση µια ενδοδιατεταγµένης µορφής µιας αριθµητικής
παράστασης µε προσθέσεις και αφαιρέσεις σε ισοδύναµη µεταδιατεταγµένη.
E→TR
R → addop T {print(addop.lexeme)}R1 | ε
T → num {print(num.val)}
Tο σχήµα 4.4 δείχνει το συντακτικό δένδρο για την παράσταση 9-5+2 όπου έχει επισυναφθεί
η αντίστοιχη σηµασιολογική ενέργεια σαν παιδί στον κόµβο που αντιστοιχεί στο αριστερό
µέλος της παραγωγής.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
34
E
T
9
R
{print('9')}
-
T
+
5
R
{print('-')}
{print('5')}
2
T
{print('+')}
{print('2')}
R
ε
Σχήµα 4.4
Oταν χρησιµοποιούµε µεταφραστικά σχήµατα πρέπει να έχουµε σιγουρέψει ότι όταν µια
ενέργεια χρησιµοποιεί την τιµή µιας attribute αυτή η τιµή είναι διαθέσιµη. Οι L-attributed
ορισµοί µας εξασφαλίζουν κάτι τέτοιο.
H απλούστερη περίπτωση είναι να έχουµε µόνο σύνθετες attributes. Τότε µπορούµε να
φτιάξουµε ένα µεταφραστικό σχήµα δηµιουργώντας µια ενέργεια που αποτελείται από µια
αντικατάσταση για κάθε σηµασιολογικό κανόνα και να µπαίνει αυτή η ενέργεια στο τέλος
του δεξιού µέλους της παραγωγής. Παραδείγµατος χάριν
T → T1*F
παραγωγή
Σηµασιoλογικός κανόνας
T.val := T1.valxF.val
δίνει την εξής παραγωγή και σηµασιολογική ενέργεια:
T → T1*F {T.val := T1.valxF.val}
Πρέπει όµως να προσέχουµε αν έχουµε και κληρονοµούµενες attributes
1) Μία κληρονοµούµενη attribute για ένα σύµβολο στα δεξιά µιας παραγωγής Πρέπει να
υπολογισθεί σε µια ενέργεια πριν από το σύµβολο
2)Μια ενέργεια δεν Πρέπει να αναφέρεται σε σύνθετη attribute ενός συµβόλου που
βρίσκεται δεξιά της.
3)Μια σύνθετη attribute για ένα µη τερµατικό στα αριστερά µπορεί να υπολογισθεί µόνο
µετά από τον υπολογισµό όλων των attributes στις οποίες αναφέρεται.
4.5 Mετάφραση από το γενικό στο ειδικό (top-down translation)
Θα χρησιµοποιήσουµε τους L-attributed ορισµούς και τα µεταφραστικά σχήµατα κατά την
συντακτική ανάλυση µε πρόβλεψη. Eπίσης, επειδή οι περισσότερες γραµµατικές για
αριθµητικές παραστάσεις είναι αριστερά αναδροµικές θα επεκτείνουµε τον αλγόριθµο
απαλοιφής της αριστερής αναδροµής και σε µεταφραστικά σχήµατα.
Έστω το µεταφραστικό σχήµα µε την γραµµατική µε αριστερή αναδροµή:
E → E1+T {E.val := E.val + T.val}
E → E1- T {E.val:=E1.val - T.val}
E → T {E.val:=T.val}
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
35
T → (E) {T.val:=E.val}
T → num {T.val:=num.val}
Αυτό µπορεί να µετασχηµατισθεί στο κατωτέρω µεταφραστικό σχήµα χωρίς αριστερές
αναδροµές:
E → T {R.i := T.val } R {E.val := R.s}
R → + T {R1.i := R.i + T.val} R1 {R.s := R1.s}
R → - T {R1.i := R.i - T.val} R1 {R.s := R1.s}
R → ε {R.s := R.i}
T → (E) {T.val:=E.val}
T → num {T.val:=num.val}
Όπου έχουµε εισάγει το σύµβολο R µε την κληρονοµούµενη attribute R.i και τη σύνθετη
R.s.Tο σχήµα 4.5 δείχνει τον υπολογισµό της 9-5+2 µε αυτό το µεταφραστικό σχήµα:
E
R.s=6
T.val=9
R.s=6
R.i=9
R.s=6
R.i=4
-T.val=5
num.val=9
R.s=6
num.val=5
-T.val=2
R.i=6
ε
num.val=2
Σχήµα 4.5
Για ανάλυση από το γενικό στο ειδικό (top down) µπορούµε να υποθέσουµε ότι µια ενέργεια
εκτελείται την ώρα που ένα σύµβολο στη ίδια θέση θα επεκτεινόταν. Παραδείγµατος χάριν
στη δεύτερη παραγωγή η αντικατάσταση (R1.i := R.i + T.val) γίνεται µετά την πλήρη
επέκταση του T µέχρι τερµατικό.
Mπορούµε να γενικεύσουµε αυτή την µέθοδο. Έστω το µεταφραστικό σχήµα:
A → A1Y {A.a:=g(A1.a,Y.y)}
A → X {A.a:=f(X.x)}
Για την απαλοιφή της αριστερής αναδροµής η γραµµατική µετασχηµατίζεται σε:
A → XR
R → YR | ε
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
36
Το µεταφραστικό σχήµα µπορεί να γραφεί:
A → X {R.i:= f(X.x)} R {A.a:= R.s}
R → Y {R1.i:=g(R.i,Y.y)} R1 {R.s:= R1.s} | ε {R.s:= R.i}
Θα εξετάσουµε ένα παράδειγµα που είδαµε και πριν. Έστω το µεταφραστικό σχήµα:
E → E1+T
E → E1-T
E→T
T → (E)
T → id
T → num
{E.nptr := mknode('+',E1.nptr,T.nptr)}
{E.nptr := mknode('-',E1.nptr,T.nptr)}
{E.nptr := T.nptr}
{T.nptr := E.nptr}
{T.nptr := mklef(id,id.entry)}
{T.nptr := mkleaf(num,num.val)}
Όταν απαλείψουµε την αριστερή αναδροµή και εφαρµόσουµε την ανωτέρω µέθοδο για το
µεταφραστικό σχήµα παίρνουµε το κατωτέρω σχήµα για την κατασκευή του συντακτικού
δένδρου.
E → T {R.i:= T.nptr} R {E.nptr:=R.s}
R → + T {R1.i:=mknode('+',R.i,T.nptr)} R1 {R.s:= R1.s}
R → - T {R1.i:=mknode('-',R.i,T.nptr)} R1 {R.s:= R1.s}
R → ε {R.s:= R.i}
T → (E) {T.nptr := E.nptr}
T → id {T.nptr := mklef(id,id.entry)}
T → num {T.nptr := mkleaf(num,num.val)}
O κατωτέρω αλγόριθµος γενικεύει την κατασκευή συντακτικών αναλυτών µε πρόβλεψη,
εφαρµόζοντας µεταφραστικά σχήµατα κατάλληλα για ανάλυση top down.
Aλγόριθµος
Eίσοδος:Eνα συντακτικά κατευθυνόµενο µεταφραστικό σχήµα µε µια γραµµατική
κατάλληλη για ανάλυση µε πρόβλεψη.
Eξοδος :Kωδικας για ένα συντακτικά κατευθυνόµενο µεταφραστή
1) Για κάθε µη τερµατικό A φτιάξε µια συνάρτηση που έχει τυπική παράµετρο για κάθε
κληρονοµούµενη attribute του A και που επιστρέφει τις τιµές των σύνθετων attributes της A
(πιθανόν σαν record).
2) Oπως στο κεφάλαιο 3 αποφασίζει πια παραγωγή να χρησιµοποιήσει µε βάση το τρέχον
σύµβολο εισόδου.
3) O κώδικας που αντιστοιχεί σε κάθε παραγωγή κάνει τα εξής. Eξετάζουµε τα tokens τα µη
τερµατικά και τις ενέργειες στο δεξιό µέλος της παραγωγής από αριστερά προς τα δεξιά
(α) Για το token X µε σύνθετη attribute x, αποθηκεύουµε την τιµή του x στη
µεταβλητή X.x.Στη συνέχεια καλούµε την αντίστοιχη ρουτίνα για ταίριασµα του token X και
προχωρούµε την είσοδο.
(β) Για το µη τερµατικό B δηµιουργούµε την αντικατάσταση c:=B(b1,b2,...,bk) µε
κλήση συνάρτησης στο δεξιό µέλος όπου τα b1,b2,...,bk είναι µεταβλητές για τις
κληρονοµούµενες attributes του B και c είναι η µεταβλητή για την σύνθετη attribute του B.
(γ) Για µια ενέργεια αντιγράφουµε τον κώδικα στον parser, αντικαθιστώντας κάθε
αναφορά σε µια attribute µε την µεταβλητή για την attribute αυτή.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
37
4.6 Eλεγχος Tυπων (Type Checking)
Eνας µεταγλωττιστή πρέπει να ελέγξει ότι ένα πρόγραµµα ακολουθεί τους συντακτικούς και
τους σηµασιολογικούς κανόνες µιας γλώσσας. O έλεγχος αυτός επαληθεύει ότι µια δοµή έχει
τον τύπο που αναµένεται από το περιεχόµενο. Παραδείγµατος χάριν στην Pascal ο τελεστής
div περιµένουµε να έχει τελούµενα integer και είναι δουλειά του ελεγκτή τύπου (type
checker) να το επαληθεύσει.
O σχεδιασµός ενός type checker για µια γλώσσα βασίζεται στην συντακτική δοµή της
γλώσσας, στους τύπους και τους κανόνες για την απόδοση τύπου σε προγραµµατιστικές
δοµές µιας γλώσσας.
O τύπος µια προγραµµατιστικής δοµής παριστάνεται µε µια έκφραση τύπου (type
expression). Μία έκφραση τύπου είτε θα είναι βασική είτε θα κατασκευάζεται από βασικές
χρησιµοποιώντας τελεστές κατασκευής τύπων (type constructors).
Ïé εκφράσεις τύπων είναι:
1) Bασικοί τύποι (integer,real κλπ).Eπίσης βασικός τύπος είναι το type_error, που δείχνει
λάθος κατά τον έλεγχο τύπου, και ο τύπος type_void που δηλώνει την µη ύπαρξη τιµής.
2) Οι εκφράσεις τύπων µπορούν να έχουν ονόµατα και το όνοµα ενός τύπου είναι έκφραση
τύπου.
3)Eνας τελεστής έκφρασης τύπου εφαρµοσµένος σε µια έκφραση τύπου είναι έκφραση
τύπου. Ïé τελεστές περιλαµβάνουν:
α) Arrays. Aν T είναι µια έκφραση τύπου, τότε το array(I,T) είναι µια έκφραση τύπου που
δηλώνει ένα array µε στοιχεία τύπου T και δείκτη I.
β) Γινόµενα. Aν T1 και T2 είναι τύποι εκφράσεις τύπων τότε και το Kαρτεσιανο γινόµενο
T1XT2 είναι έκφραση τύπου.
γ) Records: Oυσιαστικά ο τύπος µιας εγγραφής είναι το γινόµενο των τύπων των πεδίων της.
H διαφορά τους είναι ότι τα πεδία µιας εγγραφής έχουν όνοµα. O έλεγχος τύπου των
εγγραφών µπορεί να γίνει µε µια έκφραση τύπου που σχηµατίζεται εφαρµόζοντας τον
τελεστή κατασκευής τύπου record σε µια πλειάδα που σχηµατίζεται από τα ονόµατα των
πεδίων και τους αντίστοιχους τύπους. Στην PASCAL Π.χ.
type r=record
idno: integer:
name: array[1..12] of char
end:
var table : array[1..100] of r:
ορίζεται ο τύπος r που παριστά την έκφραση τύπου
record((idnoXinteger)X(nameXarray(1..12,char)))
και η µεταβλητή table που είναι ένα array από records αυτού του τύπου.
δ) Pointers: Aν T είναι µια έκφραση τύπου τότε pointer(T) είναι µια έκφραση τύπου που
σηµαίνει pointer σε ένα αντικείµενο τύπου T.
Π.χ.
var q : r :
δηλώνει την µεταβλητή q µε τύπο pointer(r).
ε) Συναρτήσεις: Μια συνάρτηση απεικονίζει ένα πεδίο ορισµού τύπου D σε ένα πεδίο τιµών
τύπου R. O τύπος µιας συνάρτησης δηλώνεται µε την έκφραση τύπου D → R. Π.χ.. η
συνάρτηση div της PASCAL έχει πεδίο ορισµού τύπου integerXinteger και πεδίο τιµών
integer. Eποµένως η div έχει τύπο:
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
38
integerXinteger → integer (υποθέτοντας ότι το X έχει µεγαλύτερη προτεραιότητα από το →.
Που σηµαίνει ότι το integerXinteger→integer είναι το ίδιο µε το (integerXinteger→integer).
Eνας τρόπος παράστασης των εκφράσεων τύπου είναι µε γράφους. Xρησιµοποιώντας την
µέθοδο του 4.3 µπορούµε να κατασκευάσουµε ένα δένδρο µε φύλα βασικούς τύπους και
εσωτερικούς κόµβους τελεστές κατασκευής τύπων. Π.χ.
→
pointer
X
char
char
integer
Σχήµα 4.6
Eνα σύστηµα τύπων (type system) είναι ένα σύνολο από κανόνες την απόδοση εκφράσεων
τύπων σε διάφορα µέρη ενός προγράµµατος. Eνας ελεγκτής τύπων (type checker) είναι η
εφαρµογή ενός τέτοιου συστήµατος.
O έλεγχος τύπου που γίνεται από τον µεταγλωττιστή λέγεται στατικός σε αντίθεση µε αυτόν
που γίνεται όταν τρέχει το µεταφρασµένο (target) πρόγραµµα που λέγεται δυναµικός. 'Sound'
λέγεται ένα σύστηµα τύπων που δεν χρειάζεται δυναµικό έλεγχο τύπων. Ìéá γλώσσα λέγεται
'ισχυρά typed' (strongle typed) όταν ο µεταγλωττιστή εξασφαλίζει, για τα προγράµµατα που
έχει µεταφράσει και δώσει για εκτέλεση, την εκτέλεση χωρίς λάθη τύπου.
4.7 Προσδιορισµός ενός ελεγκτή τύπου.
Στο υποκεφάλαιο αυτό θα περιγράψουµε ένα ελεγκτή τύπων για µια απλή γλώσσα στην
οποία ο τύπος κάθε µεταβλητής πρέπει να δηλωθεί πριν η µεταβλητή χρησιµοποιηθεί.
Έστω η κατωτέρω γραµµατική που δηµιουργεί προγράµµατα που παριστάνονται από το µη
τερµατικό P και αποτελούνται από µια σειρά από δηλώσεις D που ακολουθείται από µια
έκφραση E.
P → D;E
D → D;D | id : T
T → char | integer | array[num] of T | ↑T
E → literal | num | id | E mod E | E[E] | E↑
Eνα πρόγραµµα που µπορεί να δηµιουργηθεί από τη γραµµατική είναι:
k : integer:
k mod 1980
H γλώσσα έχει δύο βασικούς τύπους (char και integer) και χρησιµοποιούµε ένα τρίτο τύπο
(type_error) για τα λάθη. Για λόγους απλούστευσης υποθέτουµε ότι τα arrays αρχίζουν πάντα
από 1. ∆ηλ. array[24] of char; εννοούµε array[1..24] of char;. Θα προσδιορίσουµε τον
ελεγκτή τύπου µε µεταφραστικό σχήµα το οποίο θα συνθέτει τον τύπο µίας έκφρασης από
τους τύπους των υπο-εκφράσεων της.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
39
P → D;E
D → D;D
D → id : T {addtype(id.entry,T.type)}
T → char {T.type := char}
T → integer {T.type := integer}
T → array[num] of T1 {T.type := array(1..num.val, T1.type)}
T → ↑T1 {T.type := pointer(T1.type)]
Στο µεταφραστικό σχήµα η ενέργεια της παραγωγής D → id : T αποθηκεύει τον τύπο µιας
µεταβλητής στο symbol table. H ενέργεια addtype(id.entry,T.type) εφαρµόζεται στην
σύνθετη attribute entry που δείχνει στο symbol table την θέση για την id και µια έκφραση
τύπου που παριστάνεται από την σύνθετη attribute type του µη τερµατικού T.
Aν T δηµιουργεί το char η το integer τότε και η τιµή του T.type ορίζεται σε char η integer
αντίστοιχα.
Eπειδη το D εµφανίζεται πριν το E στο δεξιό µέλος της P → D;E εξασφαλίζουµε ότι οι τύποι
όλων των µεταβλητών θα έχουν αποθηκευθεί πριν ελεγχθεί η έκφραση που δηµιουργείται
από την E.
Στη συνέχεια δίνουµε κανόνες για τον προσδιορισµό της σύνθετης attribute type για
εκφράσεις που δηµιουργούνται από την E. Οι σταθερές που παριστάνονται µε tokens literal
και num είναι τύπου char και integer αντίστοιχα.
E → literal {E.type := char}
E → num {E.type := integer}
E → id {E.type := lookup(id.entry)}
H συνάρτηση lookup(e) επιστρέφει τον τύπο της µεταβλητής που δείχνεται από το e από το
symbol table.
E → E1 mod E2 {E.type := if E1.type=integer and E2.type=integer
then integer else type_error }
E →E1[E2] {E.type := if E2.type=integer and E1.type= array(s,t) then t else type_error }
Στο τελευταίο ο τύπος της έκφρασης E2 Πρέπει να είναι integer και στην περίπτωση αυτή ο
τύπος της E είναι το t που δίνεται από το array(s,t) του E1.
E → E1↑ {E.type := if E1.type= pointer(t) then t else type_error }
O τύπος του E είναι t ότι και του αντικειµένου που δείχνεται από το E.
Θα εξετάσουµε τώρα τον τύπο των εντολών. Eπειδη δοµές όπως οι εντολές δεν έχουν τιµές
θα χρησιµοποιήσουµε και τον βασικό τύπο void που δεν πρέπει να τον µπερδεύουµε µε το
type_error.
Θα εξετάσουµε µερικές απλές εντολές όπως την εντολή αντικατάστασης την εντολε if.. then,
την εντολές while.. do.
S → id := E {S.type= if id.type= E.type then void else type_error }
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
40
O τύπος της έκφρασης στο δεξιό µέλος Πρέπει να συµφωνεί µε τον τύπο της µεταβλητής στο
αριστερό.
S → if E then S1 {S.type= if E.type= boolean then S1.type else type_error }
S → while E do S1 {S.type= if E.type= boolean then S1.type else type_error }
S → S1; S2 {S.type= if S1.type= void and S2.type= void then void else type_error }
Οι κανόνες ορίζουν ότι οι εκφράσεις συνθήκες στην if και την while εντολή πρέπει να είναι
boolean.Tέλος µια ακολουθία από εντολές είναι τύπου void αν όλες οι εντολές είναι void.
Θα κλείσουµε αυτό το κεφάλαιο µε τον έλεγχο τύπου συναρτήσεων. H εφαρµογή µιας
συνάρτησης σε µια µεταβλητή µπορεί να παρασταθεί µε την παραγωγή E → E(E) όπου µια
έκφραση είναι εφαρµογή µιας έκφρασης σε µια άλλη. O κανόνας για τον έλεγχο
συναρτήσεων, σύµφωνα µε τους τελεστές που ορίσαµε στην 4.6 είναι:
E → E1(E2) {E.type= if E2.type= s and E2.type= s t then t else type_error }
Aυτό σηµαίνει ότι ο τύπος της E → E1(E2) είναι t αν ο τύπος του E2 είναι s και ο τύπος του
E1 είναι µια συνάρτηση s t από τον τύπο s του E2 στο πεδίο τιµών t.
4.8 Aσκήσεις
1) Aκολουθώντας το συντακτικά κατευθυνόµενο ορισµό της σελ.31(παράδειγµα) φτιάξτε
ένα συντακτικό δένδρο µε σχόλια για την έκφραση (3*5+6)*2.
2) Φτιάξτε το συντακτικό δένδρο για την έκφραση ((a)+(b)) σύµφωνα
α) τον συντακτικά κατευθυνόµενο ορισµό της σελ.34
β) το µεταφραστικό σχήµα της σελ.38
3) Θεωρήστε τις δηλώσεις που δηµιουργούνται από την κατωτέρω γραµµατική:
D → id L
L → ,id L | :T
T → integer | real
Γράψτε ένα µεταφραστικό σχήµα για την εισαγωγή του τύπου κάθε µεταβλητής στο symbol
table.
4) H κατωτέρω γραµµατική δηµιουργεί εκφράσεις που σχηµατίζονται εφαρµόζονται στον
τελεστή + σε ακέραιους και πραγµατικούς. Oταν προσθέτουµε δύο ακέραιους το αποτέλεσµα
είναι ακέραιος διαφορετικά είναι πραγµατικός.
E → E+T |T
T → num.num | num
α) Γράψτε ένα συντακτικά κατευθυνόµενο ορισµό για τον προσδιορισµό του τύπου κάθε
υποέκφρασης.
β) Γράψτε το α) ώστε να µεταφράζεται εκφράσεις σε µεταδιατεγµένη µορφή.
5)Γράψτε τύπους εκφράσεων για τους κατωτέρω τύπους:
α)Array από pointers σε real, όπου ο δείκτης του array είναι από 1 µέχρι 100.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
41
β) Array δύο διαστάσεων µε γραµµές από 0 µέχρι 9 και στήλες από -10 µέχρι 10.
6) Ξαναγράψτε το µεταφραστικό σχήµα για τον έλεγχο εκφράσεων της σελίδας 41 ώστε στην
περίπτωση λάθους να τυπώνεται ένα µήνυµα και να συνεχίζει τον έλεγχο σαν να είχε βρεθεί
ο αναµενόµενος τύπος.
7) Έστω η σύνθετη attribute val που δίνει την τιµή του δυαδικού αριθµού που από το S της
κατωτέρω γραµµατικής:
S →L.L | L
L → LB | B
B→0|1
(Π.χ. για 101.101 το S.val=5.625)
α) Xρησιµοποιηστε σύνθετες attributes για τον υπολογισµό του S.val.
β) Yπολογίστε το S.val µε ένα συντακτικά κατευθυνόµενο ορισµό στον οποίο η µόνη
σύνθετη attribute του B είναι η c, που δίνει τιµή τη συµµετοχή του ψηφίου που
δηµιουργείται από το B στην τελική τιµή (π.χ. η συµµετοχή του πρώτου και του τελευταίου
ψηφίου του 101.101 είναι 4 και 0.125 αντίστοιχα).
8) Yποθέστε ότι έχουµε L-attributed ορισµό µε γραµµατική LL(1). ∆είξτε ότι µπορούµε να
έχουµε τις σύνθετες και τις κληρονοµούµενες attributes στη στοίβα του parser για ένα top
down parser που καθοδηγείται από ένα πίνακα (υποκεφ.3.3).
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
42
5.Περιβάλλον εκτέλεσης προγραµµάτων
5.1 Eισαγωγή
Πρίν προχωρήσουµε στη δηµιουργία ενδιάµεσου κώδικα πρέπει νε ξετάσουµε την σχέση του
στατικού κώδικα ενός προγράµµατος µε τις ενέργειες που πρέπει να γίνουν κατά την
εκτέλεση του. Kυρίως θα εξετάσουµε την σχέση µεταξύ ονοµάτων και δεδοµένων.
Aς πάρουµε για παράδειγµα το κατωτέρω Pascal πρόγραµµα:
(1) program sort(input,output);
(2) var a : array [0..10] of integer;
(3) procedure readarray;
(4) var i : integer;
(5) begin
(6) for i := 1 to 9 do read(a[i])
(7) end;
(8) function partition (y,z: integer): integer;
(9) var i,j,x,v : integer;
(10) begin
(11) x:=a[y];
(12) i:= y; j:= z+1;
(13) while i<j do
(14) begin
(15) repeat i:=i+1 until a[i] >= x;
(16) repeat j:=j-1 until a[j] <= x;
(17) if i<j then begin
(18)
v:= a[i];
(19)
a[i]:=a[j];
(20)
a[j]:=v
(21)
end
(22) end
(23) v:=a[j]
(24) a[j]:=a[y];
(25) a[y]:=v;
(26) partition:=j
(27) end;
(28) procedure quicksort(m,n: integer);
(29) var i : integer;
(30) begin
(31) if (n>m) then begin
(32) i := partition(m,n);
(33) quicksort(m,i-1);
(34) quicksort(i+1,n)
(35) end
(36) end;
(37) begin
(38) a[0]:=-maxint; a[10] := maxint;
(39) readarray;
(40) quicksort(1,9)
(41) end.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
43
Ôï πρόγραµµα αυτό αποτελείται από procedures.H δήλωση ενός procedure στην απλούστερη
µορφή της σχετίζει ένα identifier µε µια εντολή. H εντολή είναι το σώµα του procedure ενώ ο
identifier είναι το ονοµά του. Oταν το όνοµα ενός procedure εµφανίζεται σε µια εκτελέσιµη
εντολή λέµε ότι το procedure καλείται. Ïé παράµετροι που εµφανίζονται στον ορισµό του
procedure λέγονται τυπικές παράµετροι (γράµµες 8 και 28) ενώ όταν καλείται πραγµατικές
παράµετροι (γραµµές 32,33,34, 40).
H ροή ενός προγράµµατος είναι σειριακή. ∆ηλαδή η εκτέλεση ενός προγράµµατος είναι µια
ακολουθία από βήµατα και η κλήση ενός procedure µεταφέρει control στο procedure και
µετά την εκτέλεση του procedure το πρόγραµµα συνεχίζει από το σηµείο που έγινε η κλήση
του procedure. Kάθε εκτέλεση ενός procedure αναφέρεται σαν ενεργοποίηση του.
O χρόνος ζωής µιας ενεργοποίησης ενός procedure είναι η ακολουθία των βηµάτων µεταξύ
του πρώτου και του τελευταίου βήµατος του σώµατος του procedure συµπεριλαµβανοµένου
και του χρόνου εκτέλεσης των procedures που καλεί.
¸Óôù δυο procedures a και b τότε οι χρόνοι ζωής τους η δεν επικαλύπτωνται η είναι
εγκλωβισµένοι ο ένας στον άλλο.
Eνα procedure λέγεται αναδροµικό (recursive) όταν µια ενεργοποιησή του µπορεί να αρχίσει
πριν τελειώσει µια προηγούµενη. Ìéá αναδροµική κλήση µπορεί να είναι άµεση (ένα
procedure καλεί τον εαυτο του) η εµµεση (ένα procedure καλει κάποιο άλλο που µε τη σειρά
του το καλεί). Mπορούµε να χρησιµοποιήσουµε ένα δένδρο (activation tree) τον τρόπο που
ενεργοποιούνται τα procedures. Σε ένα τέτοιο δένδρο:
α)κάθε κόµβος παριστάνει την ενεργοποίηση ενός procedure.
β)η ρίζα παριστάνει την ενεργοποίηση του κυρίως προγράµµατος.
γ)ο κόµβος του a είναι γονηός του b τότε και µόνον αν το control από το a παιρνάει στο b (το
a καλεί το b)
δ)ο κόµβος του a είναι αριστερά του κόµβου του b τότε και µόνον αν ο χρόνος ζωής του a
συµβαίνει πριν τον χρόνο ζωής του b. Ôï σχήµα 5.1 δείχνει το δένδρο που αντιστοιχεί στο
πιο πάνω παράδειγµα:
s
r
q(1,9)
p(1,9)
p(1,3)
q(1,3)
q(5,9)
p(5,9)
q(1,0)
q(2,3)
p(2,3)
q(2,1)
q(3,3)
p(7,9)
q(5,5)
q(7,9)
q(7,7)
q(9,9)
™¯ ‹Ì· 5.1
Για το σχήµα χρησιµοποιούµε s γαι την sort, r για την readarray, p για την partition και q για
την quicksort. Kατα την εκτέλεση του sort ενεργοποιείται η readarray (πρώτο παιδί του s). H
επόµενη ενεργοποίηση(αφού τελειώσει η readarray) είναι της quicksort µε πραγµατικές
παραµέτρους 1 και 9. Kατα τη διάρκεια αυτής της ενεργοποίησης οι κλήσεις των partition
και quicksort οδηγούν στην ενεργοποίηση των partition(1,9), quicksort(1,3) και
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
44
quicksort(5,9) (υποθέτουµε ότι έγινε διαχωρισµός στο 4). Ïé κλήσεις q(1,3) και q(5,9) είναι
αναδροµικές και αρχίζουν πριν τελειώσει η q(1,9).
H ροή εκτέλεσης ενός προγράµµατος αντιστοιχεί σε µια σάρωση του activation tree µε
προτεραιότηατα βάθους. Mπορούµε να χρησιµοποιήσουµε µια στοίβα (στοίβα ελέγχου) για
τον έλεγχο των ενεργών procedures. Bάζουµε στη στοίβα ένα κόµβο όταν ενεργοποιείται και
τον διαγράφουµε όταν τελειώνει. Tα περιεχόµενα της στίβας σχετίζωνατι µε τα µονοπάτια
πρός τη ρίζα του δένδρου. Oταν ο κόµβος n βρισκεται πάνω στη στοίβα η στοίβα έχει τους
κόµβους του µονοπατιου από το n στη ρίζα. Π.χ. το σχήµα 5.2 δείχνει τους κόµβους που
έχουν σαρωθέι όταν ο εέλεγχος παιρνάει στο q(2,3).
s
r
q(1,9)
p(1,9)
p(1,3)
q(1,3)
q(1,0)
q(2,3)
™¯ ‹Ì· 5.2
Tα r,p(1,9),p(1,3) και q(1,0) έχουν τελειώσει και τις παρίστάνουµε µε διακεκοµµένη γραµµή.
Ïé συνχείς γραµµές αντιστοιχούν στο µονοπάτι από το q(2,3) στη ρίζα. Στο σηµείο αυτό η
στοίβα ελεγχου θα περιέχει τα s,q(1,9),q(1,3) και q(2,3).
Ìéá δήλωση σε µια γλώσσα αντιστοιχεί κάποιες πληροφορίες σε ένα όνοµα. Ïé δηλώσεις
µπορεί να είναι άµεσες όπως στην Pascal η να είναι εξ'ορισµου (Π.χ.. ότι αρχίζει από m στην
FORTRAN είναι integer). Ôï ίδιο όνοµα µπορεί να έχει δηλωθει διαφορετικά σε διαφορετικά
µέρη ενός προγράµµατος. Σε κάθε γλώσσα υπάρχουν κανόνες που προσδιορίζουν την
εµβέλεια των µεταβλητών (scope of a variable και οι κανόνες scope rules).
Στις γλώσσες προγραµµατισµού ο όρος περιβάλλον αναφέρεται στη συνάρτηση που
απεικονίζει ένα όνοµα σε µια θέση µνήµης και ο όρος κατάσταση (state) αναφέρεται στη
συνάρτηση που απεικονίζει µια θέση µνήµης στην τιµή που αποθηκεύεται στη θέση αυτή.
Oταν σε ένα περιβάλλον αντιστοιχεί ένα όνοµα x σε µια θέση µνήµης s λέµε ότι το x είναι
'bound' στο s η πράξη δε αναφέρεται σαν προσδιορισµός διέυθυνσης (binding) του x.
Binding είναι το δυναµικό ισοδύναµο της δήλωσης.
Πριν ένας µεταγλωττιστή για µια γλώσσα προχωρήσει στην οργανωση της µνήµης Πρέπει να
έχουν ξεκαθαρισθεί τα κάτωθι:
α)Aν θα είναι αναδροµικά τα procedures.
β)Mπορούν τα procedures να παιρνάνε σαν παράµετροι.
γ)Mπορούν τα procedures να επιστρέφωνται σαν τιµές.
δ)Tι γινεται µε τις τιµές των τοπικών ονοµάτων όταν τελειώνει η ενεργοποίηση ενός
procedure.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
45
5.2 Oργάνωση Mνήµης
Aς υποθέσουµε ότι το λειτουργικό σύστηµα διαθέτει ένα block µνήµης στον µεταγλωττιστή
για να τρέξει το µεταφρασµένο πρόγραµµα. H µνήµη αυτή θα πρέπει να χρησιµοποιηθει από
τον µεταγλωττιστή για:
α)Tον κώδικα που θα δηµιουργηθεί
β)∆εδοµένα
γ)Ìéá στοίβα (αντίστοιχη της στίβας ελέγχου) για να κρατάει πληροφορίες για τα
ενεργοποιηµένα procedures.
Ôï σχήµα 5.3 δείχνει ένα χωρισµό του block.
KÒ‰Èη˜
™Ù·ÙÈο ¢Â‰Ô̤ӷ
™Ù›‚·
™ˆÚ¼˜
™¯ ‹Ì· 5.3
H απαιτούµενη µνήµη για τον κώδικα είναι σταθερή καθώς και η απαιτούµενη µνήµη για
ορισµένα στατικά δεδοµένα (σε ορισµένες γλώσσες όπως η FORTRAN όλα τα δεδοµένα
είναι στατικά). Γλώσσες όπως η Pascal και η C χρησιµοποιούν τη στοίβα για την διαχείρηση
των ενεργοποιηµένων procedures. Oταν γίνει κλήση µιας procedure γίνεται διακοπή και
πληροφορίες για την κατάσταση της µηχανής, όπως η τιµή του program counter και των
καταχωρητων(registers) αποθηκεύονται στη στοίβα. Oταν τελειώσει το procedure οι τιµές
αυτές αποκαθίστανται µε τις τιµές από τη στοίβα. Oλες οι υπόλοιπες πληροφορίες
αποθηκεύονται στο σωρό. Ôï µεγεθός της στίβας και του σωρού αλλαζει κατά την εκτέλεση
ενός προγράµµατος και για αυτό χρησιµοποιούµε τον τρόπο αποθήκευσης του 5.3 (αυξάνουν
σε αντίθετη κατεύθυνση).
Ïé πληροφορίες που πρέπει να αποθηκευθούν κατά την ενεργοποίηση ενός procedure
αποθηκεόνται σε συνεχόµενο χώρο µνήµης που λέγεται εγγραφή ενεργοποίησης (activation
record) η frame που αποτελείται από ένα σύνολο από πεδία όπως δείχνει το σχήµα 5.4.
Στα προσωρίνα δεδοµένα αποθηκευωνται οι προσωρινές τιµές όπως αυτές που
δηµιουργούνται από τον υπολογισµό εκφράσεων. Στα τοπικά δεδοµένα αποθηκεύονται τα
δεδοµένα που είναι τοπικά σε µια εκτέλεση του procedure. Στην κατάσταση της µηχανής οι
πληρόφορίες για την κατάσταση της µηχανής πριν γίνει η κλήση του procedure και που
πρέπει να αποκατασταθούν µετά την εκτλεσή του (τιµή του program counter, τιµή των
καταχωρητών). Ôï access link χρησιµοποιείται για να γίνει αναφορά σε µη τοπικά δεδοµένα
άλλων activation records. Ôï control link είναι ένας pointer στο activation record του
προγράµµατος που ενεργοποίησε αυτό το procedure. Ôï πεδίο των πραγµατικών παραµέτρων
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
46
χρησιµοποιείται από το πρόγραµµα που καλεί για να περάσει παραµέτρους και το πεδίο
returned value για να επιστρέψει τιµές το procedure που έχει κληθεί. Ôï µέγεθος αυτών των
πεδίων µπορεί να προσδιορισθεί κατά την κλήση του procedure.
TÈÌ‹ ¾Ô× Â¾ÈÛÙÚ¤ÊÂÈ
(returned value)
¶Ú·ÁÌ·ÙÈΤ˜ ¾·Ú¿ÌÂÙÚÔÈ
Control Link (¾ÚÔ·ÈÚÂÙÈμ)
Access Link (¾ÚÔ·ÈÚÂÙÈμ)
A¾ÔЋÎÂ×ÛË K·Ù¿ÛÙ·Û˘ M˯ ·Ó‹˜
TÔ¾Èο ¢Â‰Ô̤ӷ
¶ÚÔÛˆÚÈÓ¿ ¢Â‰Ô̤ӷ
™¯ ‹Ì· 5.4
.
Oσο αφορα τον χωρο της µνηµης συνηθως είναι σε blocks από 8_bit bytes. O χωρος που
απαιτειται από µια µεταβλητη εξαρτάται από τον τύπο της µεταβλητης. Eνας στοιχειωδης
(integer,char κλπ) τυπος δεδοµένων απαιτει ένα ακεραιο πλήθος από bytes. O χωρος για τα
τοπικά δεδοµένα δηµιουργείται κατά την εξέταση των procedures. *εχωριστα κραταµε τα
δεδοµένα µε µεταβλητο µέγεθος. Kραταµε ένα µετρητη µε τις θέσεις που έχουν δωθεί σε
προηγούµενα δεδοµένα και από το µετρητή βρίσκουµε τη σχετική θέση για την αποθήκευση
τοπικών τιµών.
5.3 Στρατηγικές διαχείρησης της µνήµης
∆ιαφορετικός τρόπος διαχείρησης της µνήµης εφαρµόζεται στις τρεις περιοχές δεδοµένων
του σχήµατος 5.3.
1)Στατική διαχείρηση του χώρου µε όλα τα αντικείµενα την ώρα της µετάφρασης.
2)∆ιαχείρηση µε στοίβα για την µνηµη την ώρα του τρεξίµατος ενός προγράµµατος
3)∆ιαχείρηση σωρού: δυναµική διαχείρηση της µνήµης κατά το τρέξιµο του προγράµµατος
από την περιοχή δεδοµένων του σωρού.
Στη στατική οργάνωση η σύνδεση ονοµάτων διευθύνσεων (binding) γίνεται κατά το στάδιο
της µετάφρασης και εποµένως δεν χρειάζεται υποστήριξη κατά την διάρκεια του τρεξίµατος.
Που σηµαίνει ότι σε κάθε ενεργοποίηση ενός procedure τα ονόµατα αναφερονται στην ίδια
διεύθυνση. ∆ηλαδή οι τιµές των τοπικών ονοµάτων παραµένουν κατά τις ενεργοποησεις ενός
procedure. Aπο τον τύπο του ονόµατος ο µεταγλωττιστή βρίσκει το απαιτούµενο για αυτό
µέγεθος µνήµης. Ïé διευθύνσεις αυτές εκφράζονται σχετικά από το τέλος της εγγραφής
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
47
ενεργοποίησης (activation record) αυτού του procedure. Xαρακτηριστικό παράδειγµα
γλώσσας που επιτρέπει στατική οργάνωση της µνήµης είναι η FORTRAN.
H διαχείρηση µε στοίβα στηρίζεται στην ιδέα της στίβας ελέγχου (control stack). H µνήµη
είναι οργανωµένη σα στοίβα και οι εγγρφές ενεργοποίησης προστίθενται αντίστοιχα
αφαιρούνται από τη στοίβα όταν ενεργοποιηται αντίστοιχα απενεργοποιείται. Ïé τιµές των
τοπικών ονοµάτων αποθηκεύονται στις εγγραφες ενεργοποίησης των procedures. Ïé
διευθύνσεις (binding) των τοπικών ονοµάτων είναι αυτές της τελευταίας ενεργοποίησης. Eπι
πλέον οι τιµές τους χάνονται µε την απενεργοποίηση του procedure γιατί διαγραφεται η
εγγραφή ενργοποίησης από τη στοίβα. Σε µερικές περιπτώσεις το µπορεί να είναι γνωστό το
µέγεθος της εγγραφής ενεργοποίησης αλλα µπορεί και να µην είναι.
Ôï σχήµα 5.5 δείχνει την πρόσθεση και διαγραφή activation records από τη στοίβα για το
πρόγραµµα της σελ.44.
H κληση ενός procedure γίνεται µεσω της λεγόµενης ακολουθίας κλήσης (calling sequence).
Ìéá ακολουθία κλήσης δηµιουργεί µια εγγραφη ενεργοποίησης και εισάγει πληροφορίες στα
πεδία της. Ìéá ακολουθία επιστροφής (return sequence) αποκαθιστα την κατάσταση της
µηχανής ετσι ώστε το procedure που εκανε την κλήση να µπορεί συνεχίσει την εκτέλεση.
Tοσο η δοµή των ακολουθιών κλήσης όσο και των εγγραφών ενεργοποίησης διαφέρουν
ακόµα και σε διαφορετικές υλοποιήσεις της ίδιας γλώσσας.
Oταν υπάρχουν δεδοµένα µεταβλητου µεγέθους τότε η αποθήκευση τους δεν γίνεται στο
activation record. Aποθηκεύονατι σε άλλη περιοχη και από το actvation record υπάρχει ένας
δείκτης στη διευθυνση αυτών των δεδοµένων. Ïé σχετικές διευθύνσεις αυτών των pointers
είναι γνωστές κατά την µεταφραση ετσι ώστε ο κώδικας που δηµιουργείται να έχει
προσπέλαση στα δεδοµένα αυτά µεσω των pointers.
H στρατιγική της χρησιµοποίησης στίβας δεν µπορεί να χρησιµοποιηθεί στις δυο πιο κάτω
περιπτώσεις:
1)Ïé τιµές των τοπικών ονοµάτων πρέπει να διατηρηθούν όταν τελειώσει µια ενεργοποίηση.
2)H διάρκεια ζωης του καλουµένου ξεπερνάει τη διάρκεια ζωής αυτού που καλεί. Aυτό δεν
µπορεί να συµβει σε γλώσσες όπου τα δένδρα ενεργοποίησης (activation trees) παριστάνουν
σωστάτην ροή ελέγχου µεταξύ των procedures.
Στις δυο αυτές περιπτώσεις η απελευθέρωση της µνήµης δεν µπορεί να γίνει µε διαδικάσια
LIFO (ο πρωτος που εισήχθη διαγράφεται τελευταίος) και εποµένως δεν µπορεί να
υλοποιηθεί µε στοίβα. Στην περίπτωση αυτή χρησιµοποιειται ένας σωρός στον οποίο
αποθηκευονται δεδοµένα ανάλογα µε τις απαιτήσεις χρήσης τους. Tα διαφορα κοµµατια µε
δοδοµένα µπορεί να αποδεσµευωνται µε οποιαδήποτε σειρα και εποµένως θα υπάρχουν
εναλλασόµενες περιοχές που είναι ενεργοποιηµενες. Aυτό βέβαια δεν µπορεί να υλοποιηθεί
µε στοίβα. Ôï σχήµα 5.6 δείχνει τη διαφορά του σωρού από τη στοίβα. Στο σχήµα 5.6 η
εγγραφη µιας ενεροποίησης του r παραµένει και όταν τελειώνει η ενεργοποίηση αυτή.
Eποµένως η εγγραφή ενεργοποίησης του q(1,9) δεν µπορεί να ακολουθήσει φυσικά την
εγγραφη του s όπως στο σχήµα 5.5. Aν στη συνέχεια αποδεσµευθεί η εγγραφή
ενεργοποίησης του r θα υπάρχει ελρυθερός χώρος στο σωρό µεταξύ των εγγραφών
ενεργοποίησης του s και του q(1,9). H διαχείρηση αυτού του χώρου γίνεται από το
πρόγραµµα διαχείρησης του σωρού (heap manager). Yπαρχουν διάφορες τεχνικές, από τις
δοµές δεδοµένων, για τη διαχείρηση του σωρού µε τον καλλίτερο δυνατό τρόπο. H χρήση
σωρού απαιτεί επι πλέον χώρο και χρόνο σε συγκριση µε την χρήση στίβας. H πιο
συνηθισµένη µέθοδος είναι να χρησιµοποιείται µια συνδεδεµένη λίστα µε τα ελεύθερα
blocks.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
48
£ÂÛË ÛÙÔ
activation tree
™ÙÈ‚·
s
s
a: array
s
EÓÂÚÁÔ¾Ô›ËÛË ÙÔ× s
s
a:array
r
EÓÂÚÁÔ¾Ô›ËÛË ÙÔ× r
r
i:integer
s
s
r
a:array
q(1,9)
To activation record
ÙÔ× r ¯ ÂÈ ‰È·ÁÚ·Ê› ηÈ
¯ ÂÈ ÂÓÂÚÁÔ¾ÔÈËÐÂÈ ÙÔ q
q(1,9)
k:integer
s
r
q(1,9)
s
a:array
q(1,9)
p(1,9)
q(1,3)
k:integer
OÙ·Ó Ô ÂÏÂÁ¯ Ô˜ ¾ÈÛÙÚÂÊÂ
·¾Ô ÙÔ q(1,0) ÛÙÔ q(1,3)
q(1,3)
p(1,3)
q(1,0)
k:integer
™¯ ËÌ· 5.5
Oταν ζητηθεί ένα block µεγέθους s το αίτηµα ικανοποιείται µε ένα block s' το µικρότερο σε
µέγεθος από τα ελεύθερα που είναι µεγαλύτερο η ίσο του s. Oταν το block αποδεσµευθεί
επιστρέφει στην συνδεδεµένη λίστα των ελεύθερων.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
49
Activation
¢ÂÓ‰ÚÔ
EÁÁڷʤ˜ EÓÂÚÁÔ¾Ô›ËÛË
ÛÙÔ ÛˆÚ¼
s
s
r
q(1,9)
control link
r
control link
q(1,9)
control link
™¯ ‹Ì· 5.6
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
50
6. ∆ηµιουργία Eνδιάµεσου Kώδικα
6.1 Eισαγωγή
Στην εισαγωγή ανφέραµε ότι το front end ενός µεταγλωττιστή µεταφράζει ένα πρόγραµµα σε
ενδιάµεσο κώδικα από τον οποίο το back end δηµιουργεί κώδικα µηχανής. Aν και η
κατασκευή ενδιάµεσου κώδικα δεν είναι απαραίτητη και ένα πρόγραµµα µπορεί να
µεταφρασθεί αµµεσα σε γλώσσα µηχανής υπάρχουν ορισµένα πλεονεκτήµατα όταν
δηµιουργείται ενδιάµεσος κώδικας. Tετοια πλεονεκτήµατα είναι:
α)H δυνατότητα ανάπτυξης βελτιστοποιητή (optimizer) ανεξάρτητου από το back end.
β)Eνας µεταγλωττιστή µπορεί να γραφεί για διαφορετικές µηχανές µε την προυπόθεση ότι
υπάρχει ένας back end για τις κάθε µηχανη µε αυτό το front end.
Tα συντακτικά δένδρα καθώς και η µεταδιατεταγµένη (postfix) µορφή αποτελουν δυο
µορφές ενδιάµεσου κώδικα. Στα επόµενα θα παρουσιάσουµε και µια αλλη µορφή
ενδιάµεσου κώδικα, τον κώδικα τριών διευθύνσεων. Ïé σηµασιολογικοί κανόνες για την
δηµιουργία κώδικα τριών διευθύνσεων είναι παρόµοιοι µε εκείνους για την κατασκευή
συντακτικών δένδρων.
6.2 Γραφικες παραστάσεις ενδιάµεσου κώδικα
H ιεραρχική δοµή ενός προγράµµατος παριστάνεται µε το συντακτικό δένδρο. Ôï σχήµα 6.1
δείχνει το συντακτικό δένδρο για την εντολή αντικατάστασης:
a:=b*-c+b*d
:=
a
+
*
b
*
Uminus
b
d
c
™¯ ‹Ì· 6.1
H µεταδιατεγµένη µορφή είναι µια γραµµική παράσταση του συντακτικού δένδρου. Eίναι
µια λίστα µε τους κόµβους του δένδρου στην οποία ένας κόµβος εµφανίζεται αµµεσως µετά
τα παιδιά του. H µεταδιατεταγµένη µορφή του ανωτέρω παραδείγµατος είναι:
a b c Uminus * b d * + :=
Στη γραµµική αυτή µορφή δεν εµφανίζονται οι πλευρές του δένδρου, οι οποίες µπορούν να
σχηµατισθούν από το σήµείο στο οποίο εµφανίζονται καθώς και από το πλήθος των
τελουµένων που απαιτεί ένας τελεστής. H επαναδηµιουργια των πλευρών είναι ανάλογη µε
τον υπολογισµό µιας παράστασης σε µεταδιατεταγµένη µορφή µε χρήση στίβας. Ôï
κατωτέρω παράδειγµα δηµιουργεί συντακτικά δένδρα για την εντολή αντικατάστασης:
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
51
παραγωγή
Σηµασιολογικός Kανόνας
S i d:= E
E E1+E2
E E1*E2
E -E1
E (E1)
E id
S.nptr:=mknode(':=', mkleaf(id,id.place),E.nptr)
E.nptr:=mknode('+',E1.nptr ,E2.nptr)
E.nptr:=mknode('*',E1.nptr ,E2.nptr)
E.nptr:=mkunode('Uminus',E1.nptr)
E.nptr:=E1.nptr
E.nptr:=mkleaf(id, id.place)
Ïé κανόνες αυτοί αποτελούν µια γενίκευση αυτών της 4.3. H mkunode κατασκευάζει ένα
κόµβο µε τελεστή το uminus. Ôï token id έχει µια attribute place που δείχνει στο symbol
table την είσοδο για αυτο το identifier. Για το ανωτέρω παράδειγµα οι πιο πάνω κανόνες θα
δηµιουργήσουν το συντακτικλο δένδρο 6.1. Ôï σχήµα 6.2 δείχνει δυο παραστάσεις αυτού του
δένδρου.
:=
id a
0 id
b
1 id
c
2 uminus 1
+
3 *
*
*
id
id
b
id
uminus
id
b
c
d
0
4 id
b
5 id
d
2
6 *
4
5
7 +
3
6
8 id
a
9 :=
8 7
™¯ ‹Ì· 6.2
H πρώτη παράσταση είναι µε links ενώ η δεύτερη µε array.
6.3 Kώδικας τριών διευθύνσεων
O κώδικας τριών διευθύνσεων είναι µια ακολουθία από εντολές της µορφής:
x:= y op z
όπου τα x,y και z είναι ονόµατα,σταθερές η προσωρινά ονόµατα που δηµιουργήθηκαν από
τον µεταγλωττιστή. To op είναι ένας τελεστής π.χ αριθµητικός τελεστής η λογικός τελεστής.
Ïé παραστάσεις αυτές είναι απλές καθόσον επιτρέπουν µόνο ένα τελεστή στο δεξιό µέλος.
∆ηλ. η παράσταση x+y*z δεν επιτρέπεται και θα µεταφρασθει σε:
t1 := y*z
t2 :=x+t1
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
52
όπου τα t1 και t2 είναι προσωρινά ονόµατα που έχουν δηµιουργηθεί από τον µεταγλωττιστή.
H απλη αυτή παράσταση πολύπλοκων αριθµητικών εκφράσεων καθιστα τον κώδικα τριών
διευθύνσεων επιθυµητό σαν ενδιάµεσο κώδικα. O όρος τριων διευθύνσεων προέρχεται από
το ότι συνήθως κάθε εντολη έχει τρια τελούµενα (διευθύνσεις) και ένα τελεστή. Ïé εντολές
αυτές είναι πολύ κοντα σε γλώσσα Assembly. Tετοιες εντολές είναι:
1)Eντολή αντικατάστασης που έχει τη µορφή x:= y op z όπου το op είναι ένας δυαδικός
αριθµητικός η λογικός τελεστής.
2)Eντολή αντικατάστασης που έχει τη µορφή x:= op y όπου το op είναι ένας µοναδιαίος
τελεστής.
3)Eντολή αντιγραφής x:=y
4)H εντολή αλλαγής της ροής ενόσ προγραµµατος χωρίς συνθήκη goto L. H εντολή αυτή
σηµαίνει ότι η επόµενη εντολή που θα εκτελεσθει είναι η εντολή τριών διευθύνσεων µε
εττικετα L.
5)Aλλαγή ροής µε συνθήκη : if x relop y goto L. Mε την εντολή αυτή εφαρµόζεται ένας
σχεσιακός τελεστής (<,=,<=, κλπ) µεταξύ των x και y και αν το αποτέλεσµα είναι true
εκτελειται µετά η εντολή τριών διευθύνσεων µε εττικέτα L διαφορετικά εκτελείται η αµέσως
επόµενη εντολή.
6)param x και call p,n για κλήσεις procedure και return y, όπου το y παριστα την επιστροφή
µιας τιµής. H χρήση τους είναι:
param x1
param x2
.
.
.
param xn
call p,n
που δηµιουργείται µε την κλήση του procedure p(x1,x2,...,xn). O ακέραιος n δηλώνει τον
αριθµό των πραγµατικών παραρµέτρων.
7)Eντολές αντικατάστασης µε δείκτη της µορφής: x:=y[i] η x[i]:=y. Mε την πρώτη τίθεται η
τιµή του x ιση µε την τιµή που βρίσκεται στη θέση i θέσεις µνήµης πιο κάτω από την θέση y.
8)Oταν έχουµε µια εντολή αντικατάστασης υπάρχει µια διαφορά µεταξύ δεξιού και
αριστερου µέλους. Π.χ i:= 5 το δεξιό µέλος είναι µια ακέραια τιµή ενώ το αριστερό
προσδιορίζει µια διεύθυνση.
Για το αρίστερό µέλος µιας αντικατάστασης χρησιµοποιούµε τον όρο l-value ενώ για το
δεξιό τον όρο r-value.
Xρησιµοποιούµε εντολές αντικατάστασης της µορφής x:= &y, x:= *y και *x:= y. H πρώτη
θετει σαν τιµή του x την διεύθυση του y. To y είναι πιθανα µια προσωρινή µεταβλητη που
παριστάνει µια έκφραση µε κάποια l-value και το x pointer η προσωρινή µεταβλητη. H rvalue γίνεται η l-value του y. Στην εντολή x:= *y το y είναι ένας pointer η ένα προσωρινό
ονοµα του οποίου η r-value είναι διεύθυνση. H r-value του x γίνεται ίση µε το περιεχόµενο
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
53
αυτής της θέσης. Tέλος η *x:= y θέτει την r-value του αντικειµένου που δείχνεται από το x
στην r-value του y.
Oταν χρησιµοποιούµε κώδικα τριων διευθύνσεων τότε δηµιουργούµε προσωρινά ονόµατα
για τούς εσωτερικούς κόµβους του συντακτικού δένδρου. H τιµή του µη τερµατικού E στο
αριστερό µέλος της E E1+E2 θα υπολογισθεί σε µια προσωρινή θέση t. Γενικά ο κώδικας
τριών διευθύνσεων για το id:=E αποτελείται από τον κώδικα για τον υπολογισµό της E
ακολουθούµενο από την εντολή αντικατάστασης id.place := t. Ïé παρακάτω S-attributed
ορισµός δηµιουργεί κώδικα τριων διευθύνσεων για την εντολή αντικατάστασης.
παραγωγή
Σηµασιολογικοί κανόνες
S  id:=E
E E1+E2
S.code:= E.code || gen(id.place ':=' E.place)
E.place:=newtemp; E.code:=E1.code || E2.code ||
gen(E.place ':=' E1.place '+' E2.place)
E.place:=newtemp; E.code:=E1.code || E2.code ||
gen(E.place ':=' E1.place '*' E2.place)
E.place:=newtemp; E.code:=E1.code ||
gen(E.place ':=' 'uminus' E1.place )
E.place:=E1.place; E.code:=E1.code
E.place:=id.place; E.code :=''
E
E1*E2
E
-E1
E
E
(E1)
id
H σύνθετη attribute S.code παριστάνει τον κώδικα τριων διευθύνσεων για την εντολή
αντικατάστασης S. Ôï µη τερµατικό E έχει δύο attributes, την E.place το όνοµα που θα
αποθηκευθεί η τιµή του E και την E.code δηλ. την ακολουθία εντολών τριών διευθύνσεων
για τον υπολογισµό της E. H συναρτηση newtemp επιστρέφει µια ακολουθία από διακριτα
ονόµατα,t1,t2... σε διαδοχικές κλήσεις. H συνάρτηση gen(x ':=' y '+' z) παριστάνει τον κώδικα
της x:= y+z. Aν υπάρχουν παραστάσεις αντί για τις µεταβλητές x,y και z αυτές
υπολογίζονται όταν περάσουν στην gen. Στην πράξη ο κώδικας τριών διευθύνσεων στελνεται
σε ένα αρχείο αντί για code attributes.
Ôï κατωτέρω είναι κώδικας τριών διευθύνσεων για το S while E do S1.
S
while E do S1 S.begin := newlabel;
S.after := newlabel;
S.code := gen(S.begin ':') || E.code ||
gen('if' E.place '=' '0' 'goto' S.after) || S1.code||
gen('goto' S.begin) ||
gen(S.after ':')
όπου χρησιµοποιούµε την newlabel για την δηµιουργία ετικετων (label, τα S.begin και
S.after ).
O κώδικας τριών διευθύνσεων είναι µια αφηρηµένη µορφή ενδιάµεσου κώδικα. Σε ένα
µεταγλωττιστή αυτές οι εντολές µπορούν να υλοποίηθουν σαν εγγραφές µε πεδία τον
τελεστή και τα τελούµενα. Mπορεί να χρησιµοποιηθούν εγγραφές µε τέσσερα
πεδία(τετράδες) η µε τρία πεδία(τριάδες.
Στις τετράδες χρησιµοποιούµε ένα πεδίο για τον τελεστή, δύο για τα τελούµενα και ένα για
το αποτέλεσµα. Παραδείγµατος χάριν για την παράσταση a:=b*-c+b*d έχουµε τις εγραφες:
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
54
(0)
(1)
(2)
(3)
(4)
op
uminus
*
*
+
:=
arg1
c
b
b
t2
t4
arg2
t1
d
t3
result
t1
t2
t3
t4
a
Mπορούµε όµως να χρησιµοποιήσουµε και τριάδες όπου αντί να χρησιµοποιούµε προσωρινή
µεταβλητή για το ενδιάµεσο αποτέλεσµα να αναφερόµαστε στην εντολή που έγινε ο
υπολογισµός. Για το προηγούµενο παράδειγµα σε τριάδες έχουµε:
(0)
(1)
(2)
(3)
(4)
op
uminus
*
*
+
:=
arg1
c
b
b
(1)
α
arg2
(0)
d
(2)
(3)
6.4 ∆ηλώσεις
Oταν εξετάζονται οι δηλώσεις των µεταβλητών σε ένα procedure µπορεί να δηµιουργηθεί
και ο χώρος αποθήκευσης τους. Για κάθε τοπικό όνοµα δηµιουργείται µια είσοδος στο
symbol table µε πληροφορίες όπως ο τύπος του και η σχετική διεύθυση που αποθηκεύεται η
τιµή του. Oταν το front-end δηµιουργεί διευθύνσεις πρέπει να λαβει υπόψη το back-end για
την οργάνωση της αποθήκευσης. Ôï κατωτέρω µεταφραστικό σχήµα του µη τερµατικού P
δηµιουργεί µια ακολουθία από δηλώσεις της µορφής id : T.
 {offset:=0} D
D;D
id : T {enter(id.name, T.type, offset); offset:= offset + T.width }
integer {T.type:= integer; T.width :=4 }
real {T.type:= real; T.width :=8 }
array [ num ] of T1 {T.type:= array(num.val,T1.type);
T.width:= num.valxT1.width}
T T1 {T.type:= pointer(T1.type); T.width :=4 }
P
D
D
T
T
T
Xρησιµοποιούµε µια καθολική(global) µεταβλητή την offset για να µας δείξει την επόµενη
σχετκά διαθέσιµη διεύθυση µνήµης. Aρχικά η τιµή της offset είναι 0. Kάθε φορά που ένα
καινούργιο όνοµα προστίθεται sto symbol table µπαίνει µε την τρέχουσα τιµή της offset και η
τιµή της αυξάνει όσο είναι το µέγεθος που απαιτείται για το όνοµα αυτό. Ôï procedure
enter(name,type,offset) δηµιουργεί µια καινούργια είσοδο στο symbol table για την name
αποθηκεύει τον τύπο της type και η σχετική διεύθυση offset για τα δεδοµένα της.
Xρησιµοποιούµε δυο συνθετς attributes για το µη τερµατικό T την type και την width για τον
τύπο και το µέγεθος µνήµης για το δεδοµένο αντίστοιχα. Ïé τιµές της type είναι integer (µε
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
55
απαιτούµενι χώρο 4 bytes), real (µε απαιτούµενο χώρο 8 bytes) καθώς και οι
δηµιουργούµενες από τις pointer και array.
H πρώτη παραγωγή γραφεται P  {offset:=0} D για να γίνει η αρχικοποίηση της offset.
Eναλλακτικά θα µπορούσε να γραφει:
P  MD
M ε {offset:=0}
Σε µια γλώσσα µε εγκλωβισµένα procedures, µπορούµε να ορίσουµε σχετικές διευθύνσεις σε
ονόµατα τοπικά σε ένα procedure χρησιµοποιώντας την πιο πάνω µέθοδο. Oταν εξετάζεται
ένα εσωτερικό procedure αναστέλεται η επεξεργασία των δηλώσεων του procedure στο
οποίο το περιέχει. Για να δείξουµε αυτό θα προσθέσουµε σηµασιολογικούς κανόνες στην
γλώσσα:
P D
D D; D | id: T | proc id ; D ; S (**)
Kάθε που έχουµε µια καινούργια δήλωση procedure D proc id ; D1 ; S δηµιουργείται ένα
καινούργιο symbol table και οι δηλώσεις της D1 δηµιουργούνται σ'αυτό. O καινούργιος
πίνακας έχει ένα pointer στον πίνακα του procedure στο οποίο περιέχεται. Παραδείγµατος
χάριν έστω το πρόγραµµα:
program test;
var a: integer;
x: real;
procedure A
var k: integer;
begin /*A*/
...
end; /*A*/
procedure B;
begin /*B*/
...
end; /*B*/
procedure C;
var k: integer;
procedure D;
var i:integer;
begin /*D*/
...
end /*D*/
begin /*C*/
...
end; /*C*/
begin /*test*/
...
end. /*test*/
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
56
Ôï σχήµα 6.3 δείχνει την οργάνωση αυτών των symbol table.
nil header
a
x
A
B
C
A
C
B
header
k
header
header
k
D
header D
i
™¯ ‹Ì· 6.3
O εγκλωβισµός φαίνεται από την σύνδεση (µεσω των links) των symbol tables.
Για το παράδειγµα (**) της προηγούµενης σελίδας θα δώσουµε σηµασιολογικούς κανόνες:
P MD {addwidth(top(tptr),top(offset)); pop(tptr); pop(offset)}
M ε {t:= mktable(nil); push(0,offset)}
D D1 ; D2
D proc id ; ND1; S {t:= top(tptr); addwidth(t,top(offset)); pop(tptr);
pop(offset); enterproc(top(tptr), id.name, t)}
D id : T {enterproc(top(tptr), id.name, T.type,top(offset));
top(offset):= top(offset)+T.width }
N ε {t := mktable(top(tptr)); push(t,tptr); push(0,offset)}
Σχήµα 6.4
Ïé κανόνες ορίζονται µε χρήση των κάτωθι τελεστών:
1)mktable(t): δηµιουργεί ένα καινούργιο πίνακα συµβόλων και επιστρέφει ένα pointer στον
πίνακα αυτό. Ôï t δείχνει σε ένα πίνακα που δηµιουργήθηκε πριν από αυτόν, αυτόν στον
οποίο περιέχεται. H τιµή του t αποθηκεύεται στο header στο σχήµα 6.3.
2)enter(table,name,type,offset): δηµιουργεί µια καινούργια είσοδο για το όνοµα name στον
πίνακα συµβόλων που δείχνεται από το table. Eπίσης θέτει τον τύπο type και τη σχετική
διεύθυση offset στο αντίστοιχο πεδίο.
3)addwidth(table, width): αποθηκεύει το συνολικό µέγεθος των στοιχείων του πίνακα
συµβόλων table.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
57
4)enteproc(table, name, newtable): δηµιουργεί µια καινούργια είσοδο για το procedure name
στον πίνακα συµβόλων που δείχνεται από το table. H µεταβλητη newtable δείχνει στον
πίνακα συµβόλων αυτού του procedure.
Ôï παραπάνω µεταφραστικό σχήµα δείχνει πως µπορούν να οργανωθούν τα δεδοµένα µε ένα
πέρασµα µε χρήση µιας στίβας, tptr, στην οποία κρατούνται pointers στους πίνακες
συµβόλων των procedure που περιέχουν procedure. Xρησιµοποιείται επίσης η στοίβα offset.
Ôï πάνω στοιχείο αυτής της στίβας είναι η επόµενη διαθέσιµη σχετική διεύθυση για ένα
τοπικο ονοµα του παρόντος procedure.
Ôï ανωτέρω µπορεί να επεκταθεί µε την πιο κάτω παραγωγή που επιτρέπει στο µη τερµατικό
T να δηµιουργεί και εγγραφές (εκτος από τους βασικούς ανωτέρω τύπους):
T
L
record L D end { T.type := record(top(tptr)); T.width:= top(offset);
pop(tptr); pop(offset)}
ε {t:=mktable(nil); push(t,tptr); push(0,offset)}
Oταν αναγνωρισθεί το keyword record η δεύτερη παραγωγή δηµιουργει ένα πίνακα
συµβόλων για τα ονόµατα των πεδίων. Eνας pointer σ'αυτό τον πίνακα συµβόλων µπαίνει
πάνω στη στοίβα tptr και η σχετική διεύθυση 0 µπαίνει στη στοίβα offset. H παραγωγή D
id : T εισάγει πληροφορίες για το πεδίο µε όνοµα id στον πίνακα συµβόλων της εγγραφής.
6.5 Eντολές αντικατάστασης
Στο υποκεφάλαιο αυτό θα δούµε σαν µέρος της µετάφρασης σε κώδικα τριών διευθύσεων
πώς αναζητούνται τα ονόµατα στον πίνακα συµβόλων. Ôï παρακάτω µεταφραστικό σχήµα
δείχνει πως µπορεί να γίνει η αναζήτηση ονοµάτων.
S
E
E
E
E
E
id:= E {p:= lookup(id.name);
if p nil then send(p ':=' E.place) else error }
E1+E2 {E.place := newtemp; send(E.place ':=' E1.place '+'E2.place) }
E1*E2 {E.place := newtemp; send(E.place ':=' E1.place '*'E2.place) }
-E1 {E.place := newtemp; send(E.place ':=' 'uminus' E1.place) }
(E1) { E.place := E1.place }
id {p:= lookup(id.name);
if p nil then E.place := p else error }
(6.5)
Ôï λεκτικό για το όνοµα που παριστάνεται µε το id δίνεται από το id.name. H
lookup(id.name) ελέγχει αν υπάρχει µια είσοδος για το όνοµα αυτό στον πίνακα συµβόλων.
Aν υπάρχει επιστρέφει ένα pointer στην είσοδο αυτή αν όχι την τιµή nil. H συνάρτηση send
δηµιουργει τον κώδικα σε ένα αρχείο.
H αναζητηση στον πίνακα συµβόλων γίνεται µε την lookup. Mπορούµε να περιγράψουµε τον
τρόπο λειτουργίας της ώστε να καλύπτει γλώσσες όπως η Pascal. Aς υποθέσουµε ότι το πιο
πάνω µεταφραστικό σχήµα δίνεται δίνεται µε την ακόλουθη γραµµατική:
P  MD
M ε
D D; D | id: T | proc id ; ND ; S
N ε
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
58
Για κάθε procedure που δηµιουργείται από τη γραµµατική αυτή το µεταφραστικό σχήµα 6.4
δηµιουργεί ένα καινουργιο πίνακα συµβόλων. Kάθε τέτοιος πίνακς έχει ένα pointer στο
header που δείχνει το πίνακα συµβόλων του procedure που το περιέχει. Oταν εξετασθεί η
εντολή που δηµιουργεί ένα procedure o πίνακας συµβόλων αυτού του procedure εµφανίζεται
πάνω στη στοίβα tptr. Aυτός ο pointer πηγαίνει πάνω στη στοίβα µε τις ενέργειες που
αντιστοιχούν στο µη τερµατικό N του δεξιού µέλους της D proc id ; ND ; S.
Oταν η lookup εφαρµόζεται σε ένα όνοµα πρώτα εξετάζει αν το όνοµα βρίσκεται στον
τρέχοντα πίνακα συµβόλων στον οποίο έχει προσπέλαση µέσω της top(tptr). Aν δεν το βρει
η lookup χρησιµοποιεί τον pointer στο header και βρίσκει τον πίνακα συµβόλων του
procedure που το περιέχει. Aν δεν το βρει µε τέτοιου είδους αναζητήσεις επιστρέφει την τιµή
nil.
Προσπέλαση σε arrays
H γρήγορη προσπέλαση στα στοιχεία ενός array µπορεί να γίνει αν τα στοιχεία
αποθηκευθούν σε διαδοχικές θεσεις µνήµης. Eτσι για το array A[low:max] αν κάθε στοιχείο
απαιτεί w θέσεις µνήµης και το πρώτο στοιχείο αποθηκεύεται στη θέση base τότε το i
στοιχείο αποθηκεύεται στη θέση base+(i-low)Χw που γράφεται και iΧw + (base-lowΧw).
Γενικά ένα array δυο διαστάσεων µπορεί να αποθηκευθει σε διαδοχικές θέσεις είτε κατά
στήλες (row major) η κατά γραµµες (column major).
¸Óôù το array A[l1:r1,l2:r2] τότε σε κατά γραµµές αποθήκευση το στοιχείο i,j θα αποθηκευθεί
(κατά στήλες) στη θέση (αν n2=r2-l2+1):
base+((i-l1)Χn2 + j -l2 )Χw
η οποία γράφεται
((iΧn2) + j)Χw + (base-((l1Χn2) +l2 )Χw)
όπου ο δεύτερος όρος είναι σταθερός.
Γενικα για το array A[l1:r1,l2:r2,...lk:rk] το στοιχείο A[i1,i2,...,ik] αποθηκεύεται στη θέση:
((...((i1n2+i2)n3+i3)...)nk+ik)Χw
+base-(...((l1n2+l2)n3+l3)...)nk+lk )Χw (6.6)
όπου ni=ri-li+1 και είναι σταθερό (τουλάχιστον στις γλώσσες που θεωρουµε).
Ôï προβληµα της δηµιουργίας κώδικα τριών διευθύνσεων για αναφορές σε array είναι να
σχετισθεί ο υπολογισµός (6.6) µε µια γραµµατική για προσπελάσεις σε array. Aναφορές σε
array µπορούν να γίνουν στην εντολή αντικατάστασης αν χρησιµοποιήσουµε το µη
τερµατικό L µε τις κάτωθι παραγωγές (όπου το id είναι όπως στην 6.5).
L id [Ilist] | id
Ilist Ilist , E | E
Για να διαχειρισθούµε τα όρια nj στις διαφορετικές διαστάσεις γράφουµε τις ανωτέρω
παραγωγές ως εξής:
L Ilist] | id
Ilist Ilist , E | id [E
Ïé παραγωγές αυτές επιτρέπουν να περάσουµε σαν σύνθετη attribute (array) της Ilist ένα
pointer στην είσοδο για το όνοµα του array στο symbol table. Eπίσης χρησιµοποιούµε την
σύνθετη attribute dimτης Ilist για να αποθηκεύσουµε τον αριθµό των διαστάσεων στην Ilist.
Eπισης χρησιµοποιούµε τη συνάρτηση limit(array, j) η οποία επιστρέφει την τιµή nj , τον
αριθµό των στοιχείων στη j διάσταση, του array που δείχνει στο symbol table ο pointer
array. Tέλος η attribute Ilist.place δηλώνει µια προσωρινή µεταβλητη απαραίτητη για τον
υπολογισµό των εκφράσεων του δεικτη της Ilist.
O υπολογισµός του πρώτου όρου της (6.6) γίνεται από τον αναδροµικό τύπο:
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
59
e1=i1
em= em-1Χnm+im
Eποµένως το αποτέλεσµα για m=k αρκεί να πολλαπλασιασθεί µε το w.
To L έχει δύο attributes τις L.place και L.offset. Oταν το L είναι ένα απλό όνοµα τότε η τιµή
της L.offset είναι nil και η L.place είναι ένας pointer στο symbol table για το όνοµα αυτό.
Mε βάση τα ανωτέρω θα ορίσουµε ένα µεταφραστικό σχήµα για την προσπέλαση στοιχείων
του array.
S
L := E
{ if L.offset = nil then send(L.place ' :=' E.place )
else send(L.place '[' L.offset ']' ':=' E.place ) }
E E1 + E2 {E.place :=newtemp; send(E.place ':=' E1.place +E2.place ) }
E (E1)
{ E.place :=E1.place }
E L {if L.offset = nil
then E.place := L.place
else begin
E.place :=newtemp; send(E.place ' :=' L.place '[' L.offset ']' )
end
L Ilist ]
{L.place :=newtemp; L.offset :=newtemp;
send( L.offset ':=' w '*' Ilist.place ) }
L id
{ L.place := id.place ; L.offset = nil }
Ilist Ilist1 , E {t:= newtemp; m:= Ilist1.dim+1;
send(t ':=' Ilist1.place '*' limit( Ilist1.array, m));
send(t ':=' t '+' Ilist1.place ); Ilist.array := Ilist1.array ;
Ilist.place :=t; Ilist.dim := m}
Ilist id [ E {Ilist.place := E.place ; Ilist.dim := 1; Ilist.array := id.place }
To σχήµα 6.7 δείχνει το συντακτικο δένδρο µε σχόλια για την εντολή αντικατάστασης
x:=A[y,z] όταν το A είναι ένα 10x20 array υποθέτωντας ότι η τιµή του w είναι 4. H εντολη
αντικατάστασης µεταφράζεται στις κατωτέρω εντολές τριών διευθύνσεων:
t1 :=y*20
t1:= t1+ z
t2:=A- 84
t3:= 4 *t1
t4:=t2[t3]
x:=t4
όπου χρησιµοποιήσαµε το όνοµα κάθε µεταβλητης αντί για το id.place .
Eνα πρακτικό πρόβληµα στον κώδικα τριών διευθύνσεων είναι η υπαρξη διαφορετικών
τύπων µεταβλητών σε εκφράσεις. Σε τέτοιες περιπτώσεις η θα πρέπει ο µεταγλωττιστή να
απορίπτει ορισµένες εκφράσεις µε ανάµεικτους τύπους η θα πρέπει να µετατρέπει από ένα
τύπο σε άλλο. Σαν παράδειγµα ας πάρουµε την παραγωγή: E E+E και ας θεωρήσουµε δύο
τύπους integer και real. Σύµφωνα µε το κεφάλαιο 4 χρησιµοποιούµε µια attribute type και ο
σηµασιολογικός κανόνας για την type είναι:
E
E1+E2 { E.type := if E1.type = integer and E2.type = integer then integer
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
60
else real }
O παραπάνω κανόνας όµως παραλείπει τα λάθη καθώς και τη µετατροπή από ένα τύπο σε
άλλο. Θα δώσουµε ένα πιο πλήρες παράδειγµα των σηµασιολογικών κανόνων αυτής της
παραγωγής.
A
L.place= x
L.offset= nil
E.place=t 4
:=
x
L.place= t 2
L.offset= t 3
]
Ilist.place=t 1
Ilist.dim=2
Ilist.array = A
Ilist.place=y
Ilist.dim=1
Ilist.array = A
A
,
E.place= z
L.place= x
L.offset= nil
[
E.place= y
z
L.place= y
L.offset= nil
Σχήµα 6.7
E.place :=newtemp;
if E1.type = integer and E2.type = integer
then begin send(E.place ':=' E1.place 'int +' E2.type ); E.type := integer end
else if E1.type = real and E2.type = real
then begin send(E.place ':=' E1.place 'real +' E2.type ); E.type := real end
else if E1.type = integer and E2.type = real
then begin
u:=newtemp ; send (u ':=' 'intoreal' E1.place );
send(E.place ':=' u 'real +' E2.type ); E.type := real
end
else if E1.type = real and E2.type = integer
then begin
u:=newtemp ; send (u ':=' 'intoreal' E2.place );
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
61
send(E.place ':=' E1.type ' real +' u ); E.type := real
end
else E.type :=error
όπου η συναρτηση intoreal µετατρέπει ένα integer σε real, x :=intoreal y σηµαίνε να
µετατρπεί ο integer y σε real µε την ίδια τιµή που ονµάζουµε x. Παραδείγµατος χάριν για
την x:=y+i*5 όπου τα x και y είναι real και τα i και j integer ο κώδικας τριών διευθύνσεων
είναι:
t1 := i int * j
t3:= intoreal t1
t2:= y real +t3
x:= t2
6.6 Aσκήσεις
1)Mετατρέψτε την αριθµητική παράσταση: a*-(b+c) σε
α)Eνα συντακτικό δένδρο
β)Mεταδιατεταγµένη µορφή
γ)Kώδικα τριών διευθύνσεων
2)Aποδείξτε ότι αν έχουµε µόνο δυαδικούς τελεστές τότε ένα string από τελεστές και
τελούµενα είναι σε µεταδιατεταγµένη µορφή αν ακι µόνο αν α)οί τελεστές είναι κατ ένας
λιγότεροι από τα τελούµενα και β)κάθε µη κενό πρόθεµα του string έχει λιγότερους τελεστές
από τελούµενα.
3)Tροποποιηστε το µεταφραστικό σχήµα της σελ.56, για τον υπολογισµό των τύπων και των
σχετικών διευθύνσεων ονοµάτων που δηλωνονται, ώστε να επιτρ'επονται µε λίστες
ονοµάτων αντί δηλώσεων µε απλά ονόµατα της µορής: D→ id : T.
4)Στη γλώσσα C η εντολή for έχει τη µορφή:
for (e1; e2; e3) stmt
και έχει την εξής σηµασία:
e1;
while (e2) do begin
stmt ;
e3
end
Γράψτε ένα συντακτικά κατευθυνόµενο ορισµό για να µετατρεψετε αυτή την εντολη σε
κώδικα τριών διευθύνσεων.
________________________________________________________________
© Μ. Χατζόπουλος, Γ. Κοτρώνης
Σηµειώσεις Μεταγλωττιστές 1997-98
62