Instrukcja do ćwiczenia P4 Analiza semantyczna i generowanie kodu Język: Ada Spis treści 1 Wprowadzenie 1 2 Dane i kod 2 3 Wyrażenia 2 3.1 Operacje arytmetyczne i logiczne.................. 2 3.2 Podstawowe składniki........................ 3 4 Procedury i funkcje 4 4.1 Deklaracje procedur i funkcji.................... 4 4.2 Definicje procedur i funkcji..................... 4 4.3 Wywołania procedur i funkcji.................... 5 5 Deklaracje stałych i zmiennych 5 5.1 Deklaracje zmiennych........................ 5 5.2 Deklaracje stałych.......................... 6 6 Przypisanie 6 7 Instrukcje warunkowe i pętle 6 8 Tablica symboli 6 9 Zadania do wykonania 6 1 Wprowadzenie Celem ćwiczenia jest poznanie analizy semantycznej i generowania kodu dla wybranego języka programowania. Tworzony kompilator znacznie odbiega od rzeczywistego: generowany jest kod asemblerowy, a nie kod pośredni, rozpoznawane są tylko niektóre konstrukcje składniowe, niektóre typy (głównie całkowite), brak jest optymalizacji kodu, obsługa tablicy symboli nadaje się tylko do bardzo małych programów. 1
2 Dane i kod Zmienne globalne umieszczane są w innym obszarze pamięci niż instrukcje. W programie te dwa typy konstrukcji mogą byc ze sobą przemieszane. Dlatego zapisywane są w czasie kompilacji do dwóch różnych plików. Uchwyt pliku danych uzyskujemy za pomocą funkcji get_data_file(), natomiast uchwyt pliku instrukcji za pomocą funkcji get_code_file(). 3 Wyrażenia 3.1 Operacje arytmetyczne i logiczne Argumenty operacji arytmetycznych i logicznych, a także ich wyniki, mogą być różnego rodzaju. Rodzaj ten decyduje o możliwości wystąpienia wyrażenia w określonym miejscu, np. jako parametr aktualny odpowiadający parametrowi wyjściowemu procedury lub funkcji lub jako wartość początkowa stałej. Zmienne EXPR i LOGICAL_EXPR mają wartość składającą się z rodzaju wartości (zawierającego także typ wyrażenia) i z wartości wyrażenia stałego. Rodzaj wyrażenia zapisujemy w polu t, natomiast wartość wyrażenia w polu i, f lub s w zależności od typu. Rodzaje wyrażeń: wyrażenie stałe wartość wyrażenia jest znana w czasie kompilacji, co pozwala stosować je np. jako wartości stałych. Dla takich stałych wyrażeń nie jest generowany kod; obliczona wartość wyrażenia jest przekazywana w zmiennej $$. W zależności od typu wyrażenia, rodzaj wyrażenia zapisujemy jako ET_INT_CONST, ET_FLOAT_CONST, ET_STR_CONST, ET_CHR_CONST. Wyrażenie arytmetyczne lub logiczne jest stałe, jeśli wszystkie jego argumenty są stałe. Jeżeli jeden z argumentów wyrażenia jest stały, a drugi jest zmienną lub wartością, to argument stały należy odłożyć na stos. zmienna wyrażenie jest l-wartością, czyli może wystapić np. jako parametr aktualny odpowiadający parametrowi wyjściowemu. Jako takie wyrażenie może wystąpić zmienna lub parametr procedury lub funkcji. Adres (a nie wartość) zmiennej lub parametru odkładamy na stos. Operacje arytmetyczne i logiczne na tym rodzaju wyrażeń dają wynik rodzaju wartość. Należy wówczas zdjąć ze stosu adres i umieścić tam wartość. Rodzaj wyrażenia zmienna w zależności od typu zapisujemy jako ET_INT_VAR, ET_FLOAT_VAR, ET_STR_VAR, ET_CHR_VAR. wartość wyrażenie nie ma wartości stałej, znanej w czasie kompilacji i nie jest też l-wartością. Wartość wyrażenia odkładana jest na stosie. Ten rodzaj wyrażenia w zależności od typu zapisujemy jako ET_INT_VAL, ET_FLOAT_VAL, ET_STR_VAL, ET_CHR_VAL. Kod generowany dla operacji arytmetycznych i logicznych polega na ściągnięciu pierwszego arumentu do rejestru EAX, drugiego (jeśli występuje) do rejestru EBX i wykonaniu instrukcji stosownej do operacji. Ściągnięcie argumentu do rejestru polega na ściągnięciu argumentu ze stosu (rodzaj wartość) za pomocą instrukcji POP z nazwą rejestru, załadowaniu rejestru stałą (rodzaj wyrażenie 2
stałe) za pomocą instrukcji MOV z argumentami: nazwą rejestru i stałą będącą wartością zmiennej składniowej EXPR lub załadowaniu rejestru wartością, której adres odłożony został na stosie za pomocą dwóch instrukcji: instrukcji POP z nazwą dodatkowego rejestru i instrukcją MOV przesyłającą do wybranego rejestru zawartość pamięci określoną zawartością dodatkowego rejestru. Instrukcje arytmetyczne przechowywane są w stałej tablicy op_mnemonic. Operacje arytmetyczne i logiczne realizowane są w tym okrojonym kompilatorze tylko na wartościach całkowitych. 3.2 Podstawowe składniki Podstawowe składniki wyrażeń arytmetycznych pochodzą z analizatora leksykalnego. Ich obsługa w wyrażeniach znacznie się między sobą różni. Stałe wartość stałej jest przekazywana w polu i, f lub s wartości zmiennej składniowej EXPR. Pole t tej zmiennej jest ustawiane jako ET_INT_CONST, ET_FLOAT_CONST, ET_STR_CONST, ET_CHR_CONST w zależności od typu stałej. Zmienne globalne w składni jako wywołanie procedury, rozpoznawane po rodzaju symbolu ST_SK_VAR i poziomie zagnieżdżenia równym zero. W polu s przekazywana jest nazwa zmiennej, tłumaczona przez asembler jako adres. Ten adres należy odłożyć na stos za pomocą instrukcji PUSH. Zmienne lokalne w składni jako wywołanie procedury, rozpoznawane po rodzaju symbolu ST_SK_VAR i poziomie zagnieżdżenia większym od zera. Adres zmiennej jest określany jako przesunięcie w rekordzie aktywacji procedury lub funkcji, w której ta zmienna została zadeklarowana. Reguły widoczności określają, że zmienna może być widoczna tylko jeśli została zdefiniowana w bieżącej procedurze lub funcji lub w procedurze lub funkcji tekstowo ją obejmującej. Przesunięcia w bieżącej procedurze lub funkcji obliczane są względem zawartości rejestru EBP. Zawartość tego rejestru w podprogramie bezpośrednio tekstowo otaczającym dany przechowywana jest w polu wiązania dostępu rekordu aktywacji bieżącego podprogramu. Przesunięcie pola wiązania dostępu w rekordzie aktywacji dane jest stałą ACCESS_LINK_OFFSET. Aby dostać się do zmiennych lokalnych innego podprogramu należy przejść po liczbie wiązań dostępu będącą różnicą poziomów zagnieżdżenia. Adres zmiennej należy odłożyć na stosie. Parametry wejściowe w składni jako wywołanie procedury, rozpoznawane po rodzaju symbolu ST_SK_PAR_IN. Obsługiwane podobnie jak zmienne lokalne, z tymi samymi regułami widoczności i sposobem dostępu do wartości, ale na stos odkładana jest wartość parametru, a nie jego adres. Parametry wyjściowe w składni jako wywołanie procedury, rozpoznawane po rodzaju symbolu ST_SK_PAR_IN. Obsługiwane dokładnie jak zmienne lokalne. 3
pierwszy parametr drugi parametr ostatni parametr wiązanie dostępu do rekordu aktywacji otaczającego podprogramu adres powrotu zachowany rejestr EBP EBP pierwsza zmienna lokalna druga zmienna lokalna ostatnia zmienna lokalna Rysunek 1: Rekord aktywacji podprogramu (procedury lub funkcji) 4 Procedury i funkcje 4.1 Deklaracje procedur i funkcji Deklaracja procedury dostarcza informacji o procedurze i jej parametrach, jednak nie czyni widocznymi nazw jej parametrów formalnych. Główna praca jest wykonywana w kodzie związanym ze zmienną PROC_HEADER, gdzie wstawiane są do tablicy symboli informacje o procedurze. Zanim wykona się kod związany z PROC_HEADER, wykonywany jest kod związany z parametrami formalnymi FORMAL_PARAM, o ile występują. Parametry są wstawiane do tablicy symboli, jednak w momencie wstawiania nie jest znane przesunięcie parametrów. Wartość ta jest uaktualniana przy PROC_HEADER, kiedy wszystkie parametry są już w tablicy symboli. Z kolei przy PROC_DECL nazwy parametrów muszą zostać usunięte z zakresu widzialności za pomocą funkcji ST_obscure_visibility(). 4.2 Definicje procedur i funkcji W definicji procedury lub funkcji większość pracy musi być wykonana na początku i na końcu. Środkiem zajmuje się obsługa innych zmiennych składniowych. Początkiem definicji procedury zajmuje się kod związany ze zmienną 4
PROC_HEADER_DEF. Tak jak w PROC_DECL, o wstawienie parametrów formalnych i nazwy procedury do tablicy symboli troszczy się kod związany ze zmienną PROC_HEADER. W PROC_HEADER_DEF natomiast wstawiany jest kod początku procedury: zapisanie etykiety procedury, zachowanie rejestru EBP na stosie, przepisanie wskaźnika stosu ESP do wskaźnika ramy (rejestru EBP) i zwiększenie poziomu zagnieżdżenia. Zmienna składniowa PROC_DEFINITION rozwijana jest na PROC_HEADER_DEF, DECLS, BLOCK i średnik, więc związany z nią kod jest wywoływany po obsłudze bloku, na końcu procedury. Znajduje się w nim przywrócenie zawartości rejestru EBP, instrukcja powrotu a procedury RET i zmniejszenie poziomu zagnieżdżenia. 4.3 Wywołania procedur i funkcji Ponieważ wywołanie procedury lub funkcji może wyglądać jak zmienna lub parametr, na początku kodu związanego z PROCEDURE_CALL należy sprawdzić, czy rzeczywiście jest to wywołanie podprogramu. Pierwszy element po prawej stronie reguły rozwijającej PROCEDURE_CALL to PROC_NAME. Jego podstawowym zadaniem jest wywołanie w związanym z nim kodzie funkcji ST_set_curr_subr, która zapamiętuje, którego podprogramu dotyczy wywołanie, i pozwala sprawdzić parametry aktualne wywołania. Sprawdzenie parametrów odbywa się w kodzie związanym ze zmienną ACTUAL_PARAM przez wywołanie funkcji ST_check_param(). W kodzie związanym z PROCEDURE_CALL pozostaje więc ustawienie wiązania dostępu za pomocą wywołania funkcji setup_link_access() i wpisania instrukcji wywołania podprogramu. 5 Deklaracje stałych i zmiennych 5.1 Deklaracje zmiennych Deklaracja zmiennej jest rozwijana na IDENT_LIST, dwukropek, identyfikator typu, INITIALIZATION i średnik. IDENT_LIST zwraca ciąg nazw zmiennych oddzielonych przecinkami. Dzieje się tak, ponieważ istnieje potrzeba uzyskania dostępu do nazw zmiennych i identyfikatora typu w jednej regule, jednym fragmencie kodu. Wykorzystując identyfikator typu i tablicę type_idents należy najpierw sprawdzić typ zmiennej. Potem należy sprawdzić, czy istnieje część nadająca wartości początkowe, i jeśli istnieje, czy jest odpowiedniego typu i czy jest wartością stałą. Istnieją zmienne lokalne i globalne. Rozróżniane są na podstawie bieżącego poziomu zagnieżdżenia procedur, który można uzyskać wywołując funkcję ST_get_curr_nesting_level(). Wartość zerowa oznacza zmienną globalną. Zmienne globalne przechowywane są w osobnym segmencie. Deklarujemy je podając ich nazwę jako etykietę z dwukropkiem, po czym umieszczamy dyrektywę DB, DW lub DD w zależności od typu zmiennej i wartość początkową lub wartość domyślną. Nazwa zmiennej jest zapisywana w tablicy symboli z poziomem zagnieżdżenia 0. Zmienne lokalne przechowujemy na stosie. Rezerwacja miejsca na stosie odbywa się za pomocą instrukcji PUSH z początkową lub domyślną wartością. Wpis 5
w tablicy symboli zawiera przesunięcie zmiennej na stosie względem zawartości rejestru EBP. Po każdej instrukcji PUSH to przesunięcie jest zwiększane. 5.2 Deklaracje stałych Deklaracje stałych są bardzo podobne do deklaracji zmiennych. Zmienia się rodzaj symbolu w tablicy symboli, a także mamy pojedynczą nazwę stałej, więc nie musimy jej wyodrębniać z ciągu nazw. 6 Przypisanie Kod generowany dla przypisania po pierwsze sprawdza dopasowanie typów l- wartości i r-wartości. Następnie sprawdzany jest rodzaj wartości przekazywanej w zmiennej EXPR. W zależności od niego do rejestru EBX przesyłana jest wartość albo jako arument bezpośredni, albo przez ściągnięcie ze stosu, albo przez ściągnięcie adresu ze stosu i wykorzystania go do ściagnięcia wartości. Następnie sprawdzany jest rodzaj prawej strony instrukcji przypisania. Jeśli jest to zmienna globalna, to określa ona adres, na który należy przesłać zawartość rejestru EBX. W przeciwnym wypadku jest to zmienna lokalna lub parametr podprogramu. Należy wówczas przejść po wiązaniach dostępu tyle razy, ile wynosi różnica bieżącego poziomu zagłębienia i poziomu zagłębienia zmiennej lub parametru (w szczególności zero razy), a następnie użyć przesunięcia symbolu w połączeniu z uzyskanym adresem rekordu aktywacji do wskazania adresu, pod który należy przesłać zawartość rejestru EBX. 7 Instrukcje warunkowe i pętle Człowiek całkowicie pozbawiony snu umiera. 8 Tablica symboli Plik symtab.h zawiera funkcje dostępu do tablicy symboli. Zawiera także stałe potrzebne do jego obsługi. Są tam też stałe określające rodzaje wyrażeń. Funkcje i stałe opatrzone są stosownymi komentarzami. 9 Zadania do wykonania Należy ściągnąć ze strony przedmiotu i rozpakować archiwum z plikami Makefile, ada.y, symtab.h i symtab.c. Do kompletu należy dołączyć własny plik ada.l. Następnie modyfikując zawartość pliku ada.y należy uzyskać obsługę następujących konstrukcji: 1. operacje arytmetyczne i logiczne, 2. podstawowe składniki wyrażeń, 3. deklaracje procedur, 4. definicje procedur, 6
5. wywołania procedur i funkcji, 6. deklaracje zmiennych globalnych, 7. deklaracje zmiennych lokalnych, 8. instrukcje przypisania. 7