ΑΣΚΗΣΗ Εισαγωγή Η γλώσσα Aegean-C είναι ένα υποσύνολο της γλώσσας C. Δε διαθέτει πολλούς τύπους, δομές, δείκτες, αρκετούς τελεστές, τις περισσότερες συναρτήσεις της βιβλιοθήκης και ορισμένες εντολές. Παρόλα αυτά περιλαμβάνει αρκετές δυνατότητες έτσι ώστε να μπορεί κανείς να γράψει ένα παιγνίδι όπως την προσγείωση στο φεγγάρι (Lunar Lander). Η παρακάτω συνοπτική περιγραφή της γλώσσας περιλαμβάνει μόνο τα στοιχεία που πρέπει να περιλαμβάνονται υποχρεωτικά στη γλώσσα. Η υλοποίηση της γλώσσας επιτρέπεται να προσθέσει δυνατότητες αρκεί να μην την κάνει ασύμβατη με τη βασική γλώσσα. Η σημασία όλων των στοιχείων της γλώσσας όπου δεν περιγράφεται διαφορετικά είναι ίδια με αυτή της C. Λεκτικά στοιχεία Η λεκτική δομή της Aegean-C είναι αρκετά απλή: Τα σχόλια στην Aegean-C αρχίζουν με μια διπλή κάθετο (//). Τα ονόματα των μεταβλητών και των συναρτήσεων αποτελούνται από πεζούς και κεφαλαίους χαρακτήρες και μπορούν μετά τον πρώτο χαρακτήρα να περιέχουν και ψηφία. Οι συμβολοσειρές αρχίζουν και τελειώνουν με διπλά εισαγωγικά. Μέσα στη συμβολοσειρά μπορούν να περιέχονται και οι κωδικοί διαφυγής \n και \". Όλα τα κενά αγνοούνται. Μεταβλητές Ορίζονται μόνο καθολικές ακέραιες μεταβλητές με τη σύνταξη: int varname; Εντολές Κάθε εντολή της Aegean-C τερματίζεται με ";". Πρέπει να υποστηρίζονται οι παρακάτω εντολές: έκφραση ; while (έκφραση) { εντολές if (έκφραση) { εντολές if (έκφραση) { εντολές else { εντολές Εκφράσεις Η γλώσσα Aegean-C υποστηρίζει τους παρακάτω τελεστές: o = o < > ==!= o + - o * /
Η αλλαγή της προτεραιότητας των τελεστών μπορεί να γίνει με τη χρήση παρενθέσεων. Μια έκφραση μπορεί να περιέχει κλήσεις σε συναρτήσεις. Συναρτήσεις χρήστη Ο χρήστης μπορεί να ορίσει νέες συναρτήσεις με την παρακάτω σύνταξη: όνομα_συνάρτησης() { εντολές Η συνάρτηση με το όνομα aegean αποτελεί την είσοδο του προγράμματος. Ορισμένες συναρτήσεις Στη βιβλιοθήκη της Aegean-C ορίζονται οι παρακάτω συναρτήσεις: putstring(string) Εμφανίζει τη συμβολοσειρά string στην έξοδο. Παράδειγμα putstring("hello, world\n"). putint(integer-expression) Εμφανίζει την τιμή της έκφρασης στην έξοδο. Για παράδειγμα putint(3+5) εμφανίζει 7. getint(variable) Διαβάζει έναν ακέραιο από την είσοδο του προγράμματος και θέτει την τιμή της μεταβλητής. getint() Διαβάζει έναν ακέραιο από την είσοδο του προγράμματος και τον επιστρέφει (εναλλακτική δυνατότητα υλοποίησης). Παράδειγμα Το παρακάτω παράδειγμα τυπώνει ένα ορθογώνιο τρίγωνο από αστεράκια: // A simple program in Aegean-C int numstars; int i; int j; // Print numstars stars stars() { i = 0; while (i < numstars) { putstring("*"); i = i + 1; putstring("\n"); // Program entry point aegean() { j = 0; while (j < 10) { numstars = j; stars(); j = j + 1;
Παράδειγμα εξόδου: * ** *** **** ***** ****** ******* ******** ********* Το παράδειγμα μεταγλωττισμένο Ο παρακάτω κώδικας είναι απλοποιημένη μορφή του κώδικα που παράγει ο μεταγλωττιστής της C (cc -S) για το αντίστοιχο παράδειγμα σε C..section.rodata.LC0:.string "*".LC1:.string "\n".text.align 4.globl stars.type stars:.l2:.l4:.l3:.l1: movl $0,i stars,@function movl i,%eax cmpl numstars,%eax jl.l4 jmp.l3 pushl $.LC0 call putstring addl $4,%esp incl i jmp.l2 pushl $.LC1 call putstring addl $4,%esp ret.globl aegean.type aegean,@function aegean: movl $0,j.L6: cmpl $9,j jle.l8 jmp.l7.l8: movl j,%eax movl %eax,numstars call stars incl j jmp.l6
.L7:.L5: ret.comm.comm.comm numstars,4,4 i,4,4 j,4,4 Προσθήκες Η γλώσσα Aegean-C μπορεί να επεκταθεί με πολλούς και διαφορετικούς τρόπους. Ιδιαίτερα σημαντικές θεωρούνται επεκτάσεις με δυνατότητες που δεν υπάρχουν στη γλώσσα C. Μερικές ιδέες για επεκτάσεις είναι οι παρακάτω: ορισμός τοπικών μεταβλητών πρόσθετες συναρτήσεις στη βιβλιοθήκη της γλώσσας κλήση συναρτήσεων της C πρόσθετοι τύποι μεταβλητών ορίσματα για την κλήση των συναρτήσεων πίνακες πρόσθετες δομές ελέγχου (switch, do while, break, continue, forever, goto) πρόσθετοι τελεστές Αναφορά υλοποίησης Κάθε υλοποίηση της γλώσσας Aegean-C πρέπει να συνοδεύεται από μια αναφορά υλοποίησης. Αυτή πρέπει να περιέχει τις παρακάτω ενότητες: Εισαγωγή Περιεχόμενα Η ομάδα και οι ρόλοι στην ομάδα Περιγραφή της γλώσσας o Σύντομη περιγραφή της γλώσσας με των προσθηκών που υλοποιήθηκαν o Κανονικές εκφράσεις για τα λεκτικά στοιχεία o Γραμματική της γλώσσας σε EBNF Λεκτική ανάλυση o Περιγραφή του τρόπου που υλοποιήθηκε o Προβλήματα που συναντήθηκαν και λύσεις o Σύνδεση με τα υπόλοιπα τμήματα o Πηγαίος κώδικας Συντακτική ανάλυση o Περιγραφή του τρόπου που υλοποιήθηκε o Προβλήματα που συναντήθηκαν και λύσεις o Σύνδεση με τα υπόλοιπα τμήματα o Πηγαίος κώδικας Πίνακας συμβόλων και συντακτικό δένδρο o Περιγραφή του τρόπου που υλοποιήθηκε o Δομές δεδομένων o Προβλήματα που συναντήθηκαν και λύσεις o Σύνδεση με τα υπόλοιπα τμήματα o Πηγαίος κώδικας Παραγωγή κώδικα
o Περιγραφή του τρόπου που υλοποιήθηκε o Προβλήματα που συναντήθηκαν και λύσεις o Σύνδεση με τα υπόλοιπα τμήματα o Πηγαίος κώδικας Παράδειγμα χρήσης o Ένα σύνθετο, πρωτότυπο και εντυπωσιακό πρόγραμμα σε Aegean-C o Ο τελικός του κώδικας o Αποτελέσματα εξόδου Βιβλιογραφία Πρόγραμμα εργασίας Εβδομάδα 1 Τα παρακάτω πρέπει να έχουν ολοκληρωθεί από κάθε ομάδα: Παράδοση στη Γραμματεία των μελών της ομάδας Παράδοση στο διδάσκοντα του ονόματος της ομάδας με τα μέλη της Καθορισμός αρμοδιοτήτων ανάμεσα στα μέλη Χρονικός προγραμματισμός Διάγραμμα της αναφοράς (σε Word) Περιγραφή των στοιχείων που θα υλοποιηθούν. Εβδομάδα 2 Τα παρακάτω πρέπει να έχουν ολοκληρωθεί από κάθε ομάδα: Τρόπος επικοινωνίας ανάμεσα στα τμήματα Αρχιτεκτονικός και αναλυτικός σχεδιασμός Τμήματα του κώδικα Τα αντίστοιχα τμήματα της αναφοράς Εβδομάδα 3 Τα παρακάτω πρέπει να έχουν ολοκληρωθεί από κάθε ομάδα: Έλεγχος των τμημάτων του κώδικα Πρώτες προσπάθειες συνένωσης και ολοκλήρωσης Το παράδειγμα σε Aegean-C Τα αντίστοιχα τμήματα της αναφοράς Εβδομάδα 4 Τελικός έλεγχος και παράδοση της εργασίας. Κάθε ομάδα πρέπει να αφήσει σε έναν προσβάσιμο κατάλογο που θα αναφέρεται στην αναφορά, με τακτικό τρόπο, όλα τα αρχεία που έχουν σχέση με το μεταγλωττιστή: o Πηγαίο κώδικα o Εκτελέσιμο πρόγραμμα o Παράδειγμα o Μεταγλωττισμένο παράδειγμα o Παράδειγμα σε εκτελέσιμη μορφή
Απλές στρατηγικές παραγωγής κώδικα Στις ενότητες περιγράφουμε μερικές απλές στρατηγικές για τη δημιουργία τελικού κώδικα. Οι στρατηγικές αυτές δεν παράγουν βέλτιστο κώδικα, αλλά είναι εύκολο να υλοποιηθούν. Η παραγωγή κώδικα γίνεται αναδρομικά. Ο κανόνας της συντακτικής ανάλυσης που αναγνωρίζει ολόκληρο το πρόγραμμα μπορεί να καλεί τη συνάρτηση που παράγει τον κώδικα: prog : dec_seq { codegen($1); ; dec_seq : /*... */ Η συνάρτηση αυτή καλεί αναδρομικά άλλες συναρτήσεις ανάλογα με το είδος του κόμβου του δένδρου. Εκφράσεις Η παραγωγή του τελικού κώδικα για τον υπολογισμό εκφράσεων μπορεί να γίνει εύκολα με τη χρήση ενός μοντέλου στοίβας. Σύμφωνα με το μοντέλο αυτό κάθε τελεστής λαμβάνει τους τελεστές από τη στοίβα και μεταφέρει το αποτέλεσμα πίσω στη στοίβα. Η διαδικασία αυτή ακολουθείται αναδρομικά για όλα τα στοιχεία του συντακτικού δένδρου. Στο τέλος του υπολογισμού της έκφρασης πρέπει το αποτέλεσμα που έχει μείνει στη στοίβα να αφαιρεθεί. Η υλοποίηση αυτή είναι κατάλληλη για αρχιτεκτονικές επεξεργαστών που βασίζονται σε στοίβα όπως η Java Virtual Machine και ο επεξεργαστής αριθμών κινητής υποδιαστολής της σειράς Intel Pentium, αλλά μπορεί να χρησιμοποιηθεί και σε αρχιτεκτονικές που βασίζονται σε καταχωρητές. Έτσι η έκφραση b + d υλοποιείται με τη σειρά: pushl b // Push operands pushl d pop %eax // Retrieve operand 1 pop %ebx // Retrieve operand 2 add %ebx, %eax // Perform operation push %eax // Push back result από κώδικα της μορφής: void codegen_add(struct s_tree *left, struct s_tree *right) { codegen(left); codegen(right); printf("\tpop %%eax\n"); printf("\tpop %%ebx\n"); printf("\tadd %%ebx, %%eax\n"); printf("\tpush %%eax\n"); Παράδειγμα: a = b + c * d; Δένδρο: =
a + b * c d Κώδικας: pushl a pushl b pushl c pushl d pop %eax // c * d pop %ebx mul %ebx push %eax pop %eax // b + (c * d) pop %ebx add %ebx, %eax push %eax pop %esi // a =... pop %eax mov %eax, (%esi) Βρόχοι και αποφάσεις Ο κώδικας για του βρόχους και τις αποφάσεις βασίζεται σε άλματα σε συγκεκριμένες ετικέτες. Τη δημιουργία του κώδικα διευκολύνει η υλοποίηση συνάρτησης new_label που επιστρέφει νέα ονόματα ετικετών (πιθανώς με τη χρήση των συναρτήσεων sprintf και malloc για την αποθήκευση του αποτελέσματος). Έτσι για παράδειγμα ο κώδικας για την εντολή while μπορεί να είναι της μορφής: void codegen_while(struct s_tree *expr, struct s_tree *stmt) { char *loop = new_label(); char *end = new_label(); printf("%s:\n", loop); codegen(expr); printf("\tpop %%eax\n"); printf("\tcmp %%eax, 0\n"); printf("\tje %s\n", end); codegen(stmt); printf("\tjmp %s\n", loop); printf("%s:\n", end); για να παράγει κώδικα όπως τον παρακάτω:.l0053: pushl a pop %eax cmp %eax, 0 je.l0054 // code for the loop body jmp.l0053.l0054: Συναρτήσεις Ο κώδικας για την υλοποίηση μιας συνάρτησης (π.χ. με όνομα fname) μπορεί να είναι της μορφής:.globl fname.type fname,@function fname: // procedure body
ret Αντίστοιχα η κλήση σε μια συνάρτηση (π.χ. με όνομα fname) γίνεται με την ακολουθία call fname. Μεταβλητές Για κάθε ακέραια μεταβλητή αρκεί να υπάρχει στο αρχείο της συμβολικής γλώσσας (όχι ανάμεσα σε εντολές κώδικα) μια δήλωση της μορφής:.comm varname,4,4 Η χρήση των μεταβλητών αυτών μπορεί να γίνει με απευθείας χρήση του ονόματος στις αντίστοιχες εντολές: movl %eax,varname //... cmpl varname,%eax Δημιουργία κώδικα για συμβολοσειρές Αντίθετα με ό,τι αναφέρθηκε στο εργαστήριο, ο κώδικας για τη δημιουργία των συμβολοσειρών δεν είναι απαραίτητο να φτιαχτεί σε δεύτερη φάση μεταγλώττισης. Μπορεί μια συμβολοσειρά να περιληφθεί και μέσα στις εντολές του υπόλοιπου κώδικα αρκεί να προβλεφθεί μια εντολή jmp για να μην εκτελείται η συμβολοσειρά ως κώδικας. Οι ετικέτες για τη συμβολοσειρά και τον προορισμό της jmp θα δημιουργούνται από τη συνάρτηση new_label(). Παράδειγμα για την printf:.globl main.type main: pushl %ebp movl %esp,%ebp jmp.lc2 main,@function // Skip the string data.lc0: // Label for the string data.string "hello\n" // String data.lc2: // Label for skipping pushl $.LC0 // Push the address of the string data... call printf //... for printf to print addl $4,%esp.L1: leave ret Στο παραπάνω παράδειγμα φαίνεται και ότι ο συμβολομεταφραστής μπορεί να χειριστεί τους κωδικούς διαφυγής όπως το \n χωρίς επέμβαση από το μεταγλωττιστή. Εκτέλεση και σύνδεση του μεταγλωττιστή Η εκτέλεση του μεταγλωττιστή της Aegean C (ac) μπορεί να γίνει με την παρακάτω διαδικασία: # Compile the source file lander.a to assembly file lander.s ac <lander.a >lander.s # Assemble lander.s; compile and link-in the Aegean runtime library (aegean.c) cc -o lander lander.s aegean.c Μπορεί ακόμα να αυτοματοποιηθεί με το παρακάτω Bourne shell script: #!/bin/sh # Names for the library and the compiler driver LIB=aegean.c COMP=acd
if [ "$1" = '' ] then echo "Usage: $0 basename (e.g. $0 lander)" 1>&2 echo "Will compile basename.a to basename via basename.s" 1>&2 exit 1 fi $COMP <$1.a >$1.s cc -i $1 $1.s $LIB Απαντήσεις σε συχνές ερωτήσεις 1. Χρειάζεται τελικά δεύτερη φάση δημιουργίας κώδικα για τις μεταβλητές και τις συμβολοσειρές; Όχι, α) ο κώδικας για τις μεταβλητές δημιουργείται εκτός των εντολών αφού αυτές ορίζονται έξω από τις συναρτήσεις β) για τις συμβολοσειρές υπάρχει μέθοδος δημιουργίας κώδικα που δεν απαιτεί δεύτερο πέρασμα. 2. Τι να κάνουμε με τα λάθη reduce/reduce του yacc. Ψάξτε στο αρχείο y.output για να δείτε τους κανόνες που δημιουργούν αυτά τα λάθη. 3. Τι σημαίνει το λάθος X rules never reduced; Κάποια τερματικά σύμβολα έχουν οριστεί σε κανόνες, αλλά δεν έχουν χρησιμοποιηθεί σε κανέναν άλλο κανόνα. Ψάξτε στο αρχείο y.output (στο τέλος του) για να δείτε τα μη τερματικά σύμβολα που έχουν αυτό το πρόβλημα. 4. Πως μπορούμε να βρούμε σφάλματα στο πρόγραμμά μας; Με τον απασφαλματωτή gdb. o Σε όλες τις μεταγλωττίσεις πρέπει να δώσετε το διακόπτη -g (π.χ. cc -c -g gram.c ή cc -g -o comp comp.o gram.o) o Για να τρέξετε το πρόγραμμα εκτελέστε την εντολή: o gdb name και μετά την εντολή run. 5. Εντολές 6. break 7. καθορίζει σημεία που θέλετε να σταματήσετε. π.χ. break main.c:22 8. cont 9. συνεχίζει την εκτέλεση μετά από break. 10. step 11. εκτελεί την επόμενη γραμμή κώδικα 12. next 13. εκτελεί την επόμενη γραμμή κώδικα χωρίς να μπαίνει μέσα στις συναρτήσεις 14. where 15. μετά από ένα segmentation fauls σας λέει σε ποιο σημείο έγινε. 16. print expr 17. τυπώνει την τιμή της έκφρασης expr 18. help 19. τυπώνει βοήθεια για τον gdb 20. quit 21. τερματίζει τη λειτουργία του gdb
Βιβλιογραφία Alfred V. Aho, Ravi Sethi, and Jeffrey D. Ullman. Compilers, Principles, Techniques, and Tools, pages 723-751. Addison-Wesley, 1985. Stephen C. Johnson. A tour through the portable C compiler. In UNIX Programmer's Manual. Volume 2 --- Supplementary Documents. Bell Telephone Laboratories, Murray Hill, NJ, USA, seventh edition, 1979. Brian W. Kernighan and Rob Pike. The UNIX Programming Environment. Prentice-Hall, 1984. Diomidis Spinellis. An implementation of the Haskell language. Master's thesis, Imperial College, London, UK, June 1990. Diomidis Spinellis. Implementing Haskell: Language implementation as a tool building exercise. Structured Programming, 14:37-48, 1993.