Metody realizacji języków programowania/MRJP Laboratorium: Różnice pomiędzy wersjami

Z Studia Informatyczne
Przejdź do nawigacjiPrzejdź do wyszukiwania
Mbiskup (dyskusja | edycje)
Mbiskup (dyskusja | edycje)
 
(Nie pokazano 52 wersji utworzonych przez 2 użytkowników)
Linia 1: Linia 1:
Autor: Marek Biskup (mbiskup@mimuw.edu.pl)
= Wstęp =
= Wstęp =


Tematem laboratorium metod realizacji języków programowania jest zaimplementowanie kompletnego kompilatora języka ''Kotek''.
Tematem Laboratorium Metod Realizacji Języków Programowania jest zaimplementowanie kompletnego kompilatora języka ''Kotek''.


= Język kotek =
= Język kotek =


''Kotek'' jest obiektowym językiem programowania, zbliżonym do języków takich jak ''Pascal'', czy ''Java''. Podstawowe cechy ''Kotka'' to:
''Kotek'' jest obiektowym językiem programowania zbliżonym do języków takich jak ''Pascal'' czy ''Java''. Podstawowe cechy ''Kotka'' to:


* obecność zwykłych konstrukcji takich jak pętle '''for''' i '''while''', wyrażeń arytmetycznych, logicznych
* obecność zwykłych konstrukcji takich jak pętle '''for''' i '''while''', wyrażeń arytmetycznych, logicznych,
* silne typowanie - zgodność typów jest sprawdzana w czasie kompilacji
* silne typowanie - zgodność typów jest sprawdzana w czasie kompilacji,
* możliwość deklarowania funkcji
* możliwość deklarowania funkcji,
* obecność struktur danych takich jak tablice i rekordy
* obecność struktur danych takich jak tablice i rekordy,
* możliwość deklarowania klas i tworzenia obiektów
* możliwość deklarowania klas i tworzenia obiektów,
* lokalne deklaracje funkcji i klas
* lokalne deklaracje funkcji i klas,
* dynamiczna alokacja obiektów, rekordów i tablic (na stercie)
* dynamiczna alokacja obiektów, rekordów i tablic (na stercie).


== Gramatyka ''Kotka'' ==
== Gramatyka ''Kotka'' ==
Linia 22: Linia 24:


Semantyka języka jest opisana na stronie [[/Semantyka Kotka|Semantyka ''Kotka'']]
Semantyka języka jest opisana na stronie [[/Semantyka Kotka|Semantyka ''Kotka'']]
= Wymagania implementacyjne i ocenianie =
Na 3:
Podstawowe konstrukcje, rekordy, tablice, dynamiczna alokacja
Na 4:
Lokalne deklaracje procedur.
Na 5:
Obiekty, operator '''instanceof''', rzutowanie (por. zadanie FIXME).
Dodatkowo punktowane zadania.
* Lokalne deklaracje klas,
* Odśmiecanie zamiast jawnego '''delete'''


= Zadania =
= Zadania =
Linia 44: Linia 29:
== Zadanie 0: Wybierz narzędzia i platformę docelową ==
== Zadanie 0: Wybierz narzędzia i platformę docelową ==


Wybierz platformę docelową kompilatora języka ''Kotek''. Zalecane są platforma x86 i system operacyjny Linux (kod byłby generowany w asemblerze x86, np. w formacie akceptowanym przez kompilator gcc), bądź Maszyna Virtualna Javy (kod generowany w asemblerze JVM, np. w formacie akceptowanym przez asembler Jasmin).
Wybierz platformę docelową kompilatora języka ''Kotek''. Zalecane są platforma ''x86'' i system operacyjny ''Linux'' (kod byłby generowany w asemblerze ''x86'', np. w formacie akceptowanym przez kompilator ''gcc''), bądź ''Maszyna Wirtualna Javy'' (kod generowany w asemblerze JVM, np. w formacie akceptowanym przez asembler ''Jasmin'').


Następnie wybierz język programowania, w którym będzie pisany sam kompilator. Ten język nie ma żadnego związku z platformą docelową. Zorientuj się jakie narzędzia są dostępne dla języka na który się decydujesz.
Następnie wybierz język programowania, w którym będzie pisany sam kompilator. Ten język nie ma żadnego związku z platformą docelową. Zorientuj się, jakie narzędzia są dostępne dla języka, na który się decydujesz.


* język C/C++: generator analizatorów leksykalnych ''lex'' lub ''flex'', generator parserów '''yacc''' lub '''bison''',
* język ''C/C++'': generator analizatorów leksykalnych ''lex'' lub ''flex'', generator parserów ''yacc'' lub ''bison'',
* język Java: generator analizatorów leksykalnych ''JFlex'', generator parserów ''Coco'', ''CUP'', ''Javacc'', ''ANTLR'',
* język ''Java'': generator analizatorów leksykalnych ''JFlex'', generator parserów ''Coco'', ''CUP'', ''Javacc'', ''ANTLR'',
* język Ocaml: ''ocamllex'', ''ocamlyacc''.
* język ''Ocaml'': ''ocamllex'', ''ocamlyacc''.


Niektóre narzędzia wspierają też budowanie drzewa składniowego i jego przekształcanie.  
Niektóre narzędzia wspierają też budowanie drzewa składniowego i jego przekształcanie.


== Zadanie 1: Analizator leksykalny ==
== Zadanie 1: Analizator leksykalny ==


Napisz analizator leksykalny do kompilatora języka ''Kotek''. Użyj generatora analizatorów leksykalnych (takich jak '''lex''' dla ''C'') generującego kod w języku, w którym będziesz pisał kompilator. Analizator powinien rozpoznawać konstrukcje leksykalne ''Kotka'': identyfikatory, operatory, stringi, komentarze (jeśli Twój generator analizatorów leksykalnych umożliwia obsługę zagnieżdżonych komentarzy, to powinny one być usuwane już przez ten analizator).
Napisz analizator leksykalny do kompilatora języka ''Kotek''. Użyj generatora analizatorów leksykalnych (takich jak ''lex'' dla ''C'') generującego kod w języku, w którym będziesz pisał kompilator. Analizator powinien rozpoznawać konstrukcje leksykalne ''Kotka'': identyfikatory, operatory, stałe napisowe, komentarze (jeśli Twój generator analizatorów leksykalnych umożliwia obsługę zagnieżdżonych komentarzy, to powinny one być usuwane już na tym etapie).


Przetestuj swój analizator leksykalny pisząc program, który zamienia ciąg wejściowy na ciąg leksemów, np. dla wejścia:
Przetestuj swój analizator leksykalny pisząc program, który zamienia ciąg wejściowy na ciąg leksemów, np. dla wejścia:
Linia 65: Linia 50:


  IDENTYFIKATOR("asdf") OPERATOR_PLUS IDENTYFIKATOR("qwer") OPERATOR_RAZY
  IDENTYFIKATOR("asdf") OPERATOR_PLUS IDENTYFIKATOR("qwer") OPERATOR_RAZY
  OPERATOR_RÓŻNE OPERATOR_LNAWIAZ, STRING("napis\"napis")
  OPERATOR_RÓŻNE OPERATOR_LNAWIAS, STRING("napis\"napis")


lub równoważny.
lub równoważny zapis.


== Zadanie 2: Analizator składniowy ==
== Zadanie 2: Analizator składniowy ==


Napisz parser języka Kotek wykorzystując generator parserów (np. '''yacc''' dla ''C''). Wynikiem analizy składniowej ma być drzewo składni, w którym każdy węzeł odpowiada pewnej produkcji w kodzie źródłowym (lewej stronie), a dziećmi węzła są elementy z prawej strony produkcji. Liśćmi są terminale.
Napisz parser języka ''Kotek'' wykorzystując generator parserów (np. ''yacc'' dla ''C''). Wynikiem analizy składniowej ma być drzewo składni, w którym każdy węzeł odpowiada pewnej produkcji w kodzie źródłowym (lewej stronie), dziećmi węzła są natomiast elementy z prawej strony produkcji. Liśćmi są terminale.


Dopuszczalne są modyfikacje drzewa składniowego, które upraszczają jego strukturę, a nie gubią żadnych istotnych informacji.  Np. zamiast przechowywania listy deklaracji jako węzła, który zawiera pojedynczą deklarację i drugą listę deklaracji, dopuszczalne (a nawet zalecane) jest używanie standardowych kontenerów języka programowania, w którym kompilator jest pisany.
Dopuszczalne są modyfikacje drzewa składniowego, które upraszczają jego strukturę, a nie gubią żadnych istotnych informacji.  Np. zamiast przechowywania listy deklaracji jako węzła, który zawiera pojedynczą deklarację i drugą listę deklaracji, dopuszczalne (a nawet zalecane) jest używanie standardowych kontenerów języka programowania, w którym kompilator jest pisany.


Jeśli używasz języka obiektowego zalecane jest stworzenie oddzielnej klasy dla każdego typu produkcji. Obiekty należy w miarę możliwości i potrzeb powiązać związkami dziedziczenia. Zwykle dobrym pomysłem jest stworzenie oddzielnej klasy dla lewej strony produkcji i podklas dziedziczących z niej dla każej prawej strony produkcji, w której ta lewa strona występuje. Na przykład możemy mieć klasę ''Intrukcja'', z której dziedziczą m.in. klasy ''Przypisanie'', ''Blok'', ''PustaInstrukcja'' i ''Złożona instrukcja''. Z kolei z klasy ''ZłożonaInstrukcja'' będą dziedziczyć klasy ''InstrukcjaIfElse'', ''InstrukcjaIf'' oraz ''InstrukcjaWhile''.
Jeśli używasz języka obiektowego, zalecane jest stworzenie oddzielnej klasy dla każdego typu produkcji. Obiekty należy w miarę możliwości i potrzeb powiązać związkami dziedziczenia. Zwykle dobrym pomysłem jest stworzenie oddzielnej klasy dla lewej strony produkcji i podklas dziedziczących z niej dla każdej prawej strony produkcji, w której ta lewa strona występuje. Na przykład możemy mieć klasę ''Instrukcja'', z której dziedziczą m.in. klasy ''Przypisanie'', ''Blok'', ''PustaInstrukcja'' i ''ZłożonaInstrukcja''. Z kolei z klasy ''ZłożonaInstrukcja'' będą dziedziczyć klasy ''InstrukcjaIfElse'', ''InstrukcjaIf'' oraz ''InstrukcjaWhile''.


Hierarchia klas węzłów drzewa składniowego powinna mieć wspólny korzeń (nazwany np. ''Węzeł''). Ten węzeł będzie zawierał abstrakcyjne metody, które są wymagane w innych węzłach, np. metoda ''gen()'', generująca kod na maszynę docelową, metoda ''sem()'' przeprowadzająca analizę semantyczną (uwaga, jeśli analiza semantyczna będzie robiona w kilku przebiegach, to warto stworzyć więcej metod ''sem'', np. ''sem1'' i ''sem2'').
Hierarchia klas węzłów drzewa składniowego powinna mieć wspólny korzeń (nazwany np. ''Węzeł''). Ten węzeł będzie zawierał abstrakcyjne metody, które są wymagane w innych węzłach, np. metoda ''gen()'', generująca kod na maszynę docelową, metoda ''sem()'' przeprowadzająca analizę semantyczną (uwaga: jeśli analiza semantyczna będzie robiona w kilku przebiegach, to warto stworzyć więcej metod ''sem'', np. ''sem1'' i ''sem2'').


Parser powinien działać jak ''pretty printer'' - powinien wypisywać treść programu na podstawie drzewa składniowego zawartego w pamięci.
Napisz program testujący parser. Program testujący powinien działać jak ''pretty printer'', tzn. wypisywać treść programu na podstawie drzewa składniowego zawartego w pamięci.


=== Zadanie 2.a: Poprawność wyrażeń na etapie parsowania ===
=== Zadanie 2.a: Poprawność wyrażeń na etapie parsowania ===
Linia 88: Linia 73:
  zmienna[indeks](param)
  zmienna[indeks](param)


Niepoprawność wynika z tego, że funkcja nie może być wynikiem innej funkcji, ani funkcja nie może być przechowywana w tablicy. Czy można zmienić gramatykę tak, aby wykluczała podane wyrażenia? Jakie podejście preferujesz: sprawdzanie poprawności tych wyrażeń na etapie parsera, czy analizy semantycznej? Podaj zalety i wady obu metod.
Niepoprawność wynika z tego, że funkcja nie może być wynikiem innej funkcji, funkcja nie może też być przechowywana w tablicy. Czy można zmienić gramatykę tak, aby wykluczała podane wyrażenia? Jakie podejście preferujesz: sprawdzanie poprawności tych wyrażeń na etapie parsera czy analizy semantycznej? Podaj zalety i wady obu metod.


=== Zadanie 2.b: Poprawność przypisań ===
=== Zadanie 2.b: Poprawność przypisań ===
Linia 98: Linia 83:
dopuszcza pewne błędne przypisania, np.  
dopuszcza pewne błędne przypisania, np.  


  "tekst" := 8
  "tekst" := 8;


Czy można tak zmienić gramatykę, aby przypisanie zawsze było poprawne? Czy pozwoli to wyeliminować analizę sematyczną tego fragmentu kodu źródłowego?
Czy można tak zmienić gramatykę, aby przypisanie zawsze było poprawne? Czy pozwoli to wyeliminować analizę semantyczną tego fragmentu kodu źródłowego?


== Zadanie 3: Tablica symboli ==
== Zadanie 3: Tablica symboli ==


Do Twojego kompilatora dopisz kod tworzący tablicę symboli z każdej listy deklaracji. Połącz tablice syboli w graf, zgodnie z regułami widoczności nazw. Tablica symboli powinna mieć odniesienie do tablicy symboli nadrzędnej funkcji lub klasy, w której nasza funkcja (lub klasa) została zadeklarowana. W przypadku tablicy symboli klasy dodaj jeszcze odnośniki do tablicy symboli nadklasy. Określ kolejność szukania nazw - najpierw nadklasa, czy nadfunkcja.
Do Twojego kompilatora dopisz kod tworzący tablicę symboli z każdej listy deklaracji. Połącz tablice symboli w graf zgodnie z regułami widoczności nazw. Tablica symboli powinna mieć odniesienie do tablicy symboli nadrzędnej funkcji lub klasy, w której nasza funkcja (lub klasa) została zadeklarowana. W przypadku tablicy symboli klasy dodaj jeszcze odnośniki do tablicy symboli nadklasy. Określ kolejność szukania nazw (najpierw nadklasa, czy nadfunkcja?).


Dla każdej deklaracji określ rozmiar zużytej pamięci, np. w architekturze x86, 32 bitowej zmienne typu '''int''' zużywają 4 bajty, zmienne '''string''' również 4 bajty (napis jest reprezentowany przez wskaźnik do ciągu znaków zakończonego zerem), tak samo zmienne typu obiektowego, tablicowego i rekordowego. Funkcje, rekordy, typy i klasy nie zajmują żadnego miejsca (w kotku nie są z nimi skojarzone żadne modyfikowalne wartości). Oczywiście rozmiar pamięci zależy od docelowej architektury. Dla każdej kolejnej deklaracji określ jej położenie w pamięci względem pierwszego elementu. Np, na wspomnianej wcześniej architekturze dla deklaracji:
Dla każdej deklaracji określ rozmiar zużytej pamięci, np. w 32 bitowej architekturze x86 zmienne typu '''int''' zużywają 4 bajty, zmienne '''string''' również 4 bajty (napis jest reprezentowany przez wskaźnik do ciągu znaków zakończonego zerem) i tak samo zmienne typu obiektowego, tablicowego i rekordowego. Deklaracje funkcji, rekordów, typów i klas nie zajmują żadnego miejsca (w ''Kotku'' nie są z nimi skojarzone żadne modyfikowalne wartości). Oczywiście rozmiar pamięci zależy od docelowej architektury. Dla każdej kolejnej deklaracji określ jej położenie w pamięci względem pierwszego elementu. Na przykład na wspomnianej wcześniej architekturze dla deklaracji:


  '''var''' x : '''int''';
  '''var''' x : '''int''';
Linia 113: Linia 98:
  '''var''' z : '''int''';
  '''var''' z : '''int''';


położenie zmiennej x to 0, zmiennej y to 4, funkcji f to 4 (choć ta wartość nie będzie do niczego potrzebna i można ją opuścić), a zmiennej z to 8.
położenie zmiennej ''x'' to 0, zmiennej ''y'' to 4, funkcji ''f'' to 4 (choć ta wartość nie będzie do niczego potrzebna i można ją opuścić), a zmiennej ''z'' to 8.


Tablica symboli powinna udostępniać następujące funkcje:
Tablica symboli powinna udostępniać następujące funkcje:
* znajdowanie identyfiktora - określenie w której tablicy symboli jest zawarty,
* znajdowanie identyfikatora - określenie, w której tablicy symboli jest zawarty,
* określanie pozycji identyfikatora w tablicy symboli,
* określanie pozycji identyfikatora w tablicy symboli,
* podawanie całkowitego rozmiaru deklaracji w tablicy symboli.
* podawanie całkowitego rozmiaru wszystkich deklaracji z tablicy symboli.


Rozbuduj ''pretty printer'' z poprzedniego zadania tak, aby obok każdego identyfikatora  wypisywał z której tablicy symboli identyfikator pochodzi i jakie jest jego przesunięcie w tej tablicy symboli. Nie musisz na razie uwzględniać operatora kropki (odwołanie do pól obiektu). Określ sam czytelny format danych wyjściowych.
Rozbuduj ''pretty printer'' z poprzedniego zadania tak, aby obok każdego identyfikatora  wypisywał, z której tablicy symboli identyfikator pochodzi i jakie jest jego przesunięcie w tej tablicy symboli. Nie musisz na razie uwzględniać operatora kropki (odwołanie do pól obiektu). Określ sam czytelny format danych wyjściowych.


Przetestuj swój kod na trzech przykładach. Przykłady powinny zawierać wszystkie następujące konstrukcje: dziedziczenie, funkcje lokalne, zmienne lokalne, przysłanianie zmiennej, argumenty funkcji, obiekty lokalne wewnątrz funkcji i innych obiektów, rekordy.
Przetestuj swój kod na trzech przykładach. Przykłady powinny zawierać wszystkie następujące konstrukcje: dziedziczenie, funkcje lokalne, zmienne lokalne, przysłanianie zmiennej, argumenty funkcji, obiekty lokalne wewnątrz funkcji i innych obiektów, rekordy.
Linia 126: Linia 111:
== Zadanie 4: Analiza semantyczna ==
== Zadanie 4: Analiza semantyczna ==


Do swojego kompilatora dopisz kod odpowiedzialny za analizę semantyczną. Głównym zadaniem analizy semantycznej jest określenie typu każdego węzła (niektóre konstrukcje nie mają typu; wtedy typem węzła będzie '''void''') oraz sprawdzanie poprawności typów. Dodatkowo musisz sprawdzać wszystkie inne cechy semantyczne 'Kotka'. Opracowanie kompletnej listy wszystkich rzeczy, które należy sprawdzać jest skomplikowane, więc nie przejmuj się, jeśli o czymś zapomnisz. Zawsze możesz uzupełnić kod analizy semantycznej w czasie pisania generatora kodu. Zajrzyj do zadań - być może pomogą Ci lepiej zrozumieć semantykę języka 'Kotek'.
Do swojego kompilatora dopisz kod odpowiedzialny za analizę semantyczną. Głównym zadaniem analizy semantycznej jest określenie typu każdego węzła (niektóre konstrukcje nie mają typu; wtedy typem węzła będzie '''void''') oraz sprawdzanie poprawności typów. Dodatkowo musisz sprawdzać wszystkie inne cechy semantyczne 'Kotka'. Opracowanie kompletnej listy wszystkich rzeczy, które należy sprawdzać jest skomplikowane, więc nie przejmuj się, jeśli o czymś zapomnisz. Zawsze możesz uzupełnić kod analizy semantycznej w czasie pisania generatora kodu. Zajrzyj do zadań z tego rozdziału - być może pomogą Ci lepiej zrozumieć zawiłości semantyki języków programowania.


Na początku przyjmij, że nie jest dozwolona deklaracja klas wewnątrz innych klas i procedur. Następnie, rozwiązując poniższe podzadania, zastanów się jakie ograniczenia należy wprowadzić aby definiowanie klas wewnątrz klas i procedur miało sens.
Na początku przyjmij, że nie jest dozwolona deklaracja klas wewnątrz innych klas i procedur. Następnie, rozwiązując poniższe podzadania, zastanów się jakie ograniczenia należy wprowadzić, aby definiowanie klas wewnątrz klas i procedur miało sens.


Rozszerz program testowy kompilatora (który po poprzednim zadaniu wypisuje tablicę symboli, z której pochodzi każdy symbol wraz z przesunięciem) o wypisywane typu każdego identyfikatora. Dopisz obsługę rekordów i obiektów (operator kropki).
Rozszerz program testowy kompilatora (który po poprzednim zadaniu wypisuje tablicę symboli, z której pochodzi każdy symbol wraz z przesunięciem) o wypisywane typu każdego identyfikatora. Dopisz obsługę rekordów i obiektów (operator kropki).


=== Zadanie 4.a: Klasy wewnątrze procedur ===
=== Zadanie 4.a: Klasy wewnętrzne procedur ===


W tym zadaniu rozważamy jedynie procedury, które nie są metodami klasy ani nie są zawarte z jakiejś metodzie.
W tym zadaniu rozważamy jedynie procedury, które nie są metodami klasy ani nie są zawarte z jakiejś metodzie.


Klasy zadeklarowane lokalnie wewnątrz procedur przydają się do zmniejszenia liczby globalnych nazw oraz do ścisłego określenia zakresu użycia klasy. Udostępnienie takich konstrukcji prowadzi do dodatkowych problemów implementacyjnych, podobnych do problemów z zagnieżdżonymi procedurami.
Klasy zadeklarowane lokalnie wewnątrz procedur przydają się do zmniejszenia liczby globalnych nazw oraz do ścisłego określenia zakresu użycia klasy. Udostępnienie takich konstrukcji prowadzi do dodatkowych problemów implementacyjnych podobnych do problemów z zagnieżdżonymi procedurami.


Pytania:
Pytania:
* Podaj przykład programu, w którym występuje funkcja zwracająca instancję lokalnie zadeklarowanej klasy.
* Podaj przykład programu, w którym występuje funkcja zwracająca instancję lokalnie zadeklarowanej klasy.
* Czy do zewnętrznych zmiennych można odwoływać się za pomocą tablicy display, jak w przypadku procedur?
* Czy do zewnętrznych zmiennych można odwoływać się za pomocą tablicy display, jak w przypadku procedur?
* Język Java dopuszcza korzystanie z lokalnych zmiennych procedury, ale tylko ale zmiennych zadeklarowanych jako final. Dlaczego takie rozwiązanie upraszcza implementację?
* Język ''Java'' dopuszcza korzystanie z lokalnych zmiennych procedury, ale tylko zmiennych zadeklarowanych jako '''final'''. Dlaczego takie rozwiązanie upraszcza implementację?
* Czy dostęp do zmiennych globalnych utrudnia implementację?
* Czy dostęp do zmiennych globalnych utrudnia implementację?
* Jakie jest możliwe rozwiązanie ogólnego przypadku? Czy widzisz związek z językami funkcyjnymi?
* Jakie jest możliwe rozwiązanie ogólnego przypadku? Czy widzisz związek z językami funkcyjnymi?
Linia 147: Linia 132:
=== Zadanie 4.b: Klasy wewnątrz klas ===
=== Zadanie 4.b: Klasy wewnątrz klas ===


Kolejnym krokiem do zwiększenia modularności jest dopuszczenie deklaracji klasy wewnątrz klas (na razie nie wewnątrz metody klasy). Taka klasa wewnętrzna miałaby dostęp do metod i zmiennych klasy wewnątrz której jest zadeklarowana.
Kolejnym krokiem do zwiększenia modularności jest dopuszczenie deklaracji klasy wewnątrz klas (na razie nie wewnątrz metody klasy). Taka klasa wewnętrzna miałaby dostęp do metod i zmiennych klasy, wewnątrz której jest zadeklarowana.


* Zaproponuj sposób implementacji dla takich klas.
* Zaproponuj sposób implementacji dla takich klas.
* Kiedy jest możliwe utworzenie instancji klasy wewnętrznej bez klasy zewnętrznej
* Kiedy jest możliwe utworzenie instancji klasy wewnętrznej bez klasy zewnętrznej?


=== Zadanie 4.c: Dowolne, lokalne deklaracje klas ===
=== Zadanie 4.c: Dowolne, lokalne deklaracje klas ===
Linia 157: Linia 142:


* Jakie informacje musi nieść ze sobą klasa zadeklarowana wewnątrz innej klasy (być może wewnątrz jakiejś metody tamtej klasy lub wewnątrz funkcji zdefiniowanej w metodzie)?
* Jakie informacje musi nieść ze sobą klasa zadeklarowana wewnątrz innej klasy (być może wewnątrz jakiejś metody tamtej klasy lub wewnątrz funkcji zdefiniowanej w metodzie)?
* Jakie informacje musi nieść ze sobą klasa zadeklarowana wewnątrz pewnej funkcji (być może funkcja jest metodą klasy)?
* Jakie informacje musi nieść ze sobą klasa zadeklarowana wewnątrz pewnej funkcji (być może funkcja jest metodą innej klasy)?


== Zadanie 5: Generowanie kodu ==
=== Zadanie 4.d: Inicjalizacja zmiennych ===
 
== Zadanie 6: Rozszerzenia języka ==
 
Język kotek w obecnej postaci jest raczej ubogi. Pożądane jest rozszerzenie go.
 
 
=== Zadanie 6.a:
 
Dodaj do języka typy zmiennoprzecinkowe ('''double''' - podwójnej prezycji i '''float''' - pojedynczej prezycji) oraz logiczny ('''bool'''). Pamiętaj, że musisz wprowadzić poprawki do kompilatora na wszystkich poziomach, począłwszy od analizy leksykalnej, a skończywszy na generowaniu kodu.
 
=== Zadanie 6.b:
 
Dodaj nowe operatory dla wyrażeń, znane z języka ''C'': '''++''', '''--''', '''+=''' (i pokrewne), operatory bitowe ('''|''', '''&''').
 
 
=== Zadanie 6.c:
 
Zrób automatyczne odśmiecanie niedostępnych obiektów (zamiast instrukcji '''delete'''). Najlepiej użyj gotowego odśmiecacza (ang. '''Garbage Collector'''), choć możesz pokusić się o własną implementację
 
 
== Zadanie 6.d: Inicjalizacja zmiennych ==


Aby język był bardziej bezpieczny chcielibyśmy wyeliminować możliwość użycia niezainicjalizowanych zmiennych. W tym celu modyfikujemy deklaracje zmiennej tak, aby podanie wartości przy deklaracji było obowiązkowe:
Aby język był bardziej bezpieczny chcielibyśmy wyeliminować możliwość użycia niezainicjalizowanych zmiennych. W tym celu modyfikujemy deklaracje zmiennej tak, aby podanie wartości przy deklaracji było obowiązkowe:
Linia 198: Linia 162:


Przemyśl następujące możliwości:
Przemyśl następujące możliwości:
* dopuszczenie tylko wyrażeń stałych
* dopuszczenie tylko wyrażeń stałych,
* dopuszczenie jedynie zmiennych zadeklarowanych wcześniej na tym samym poziomie
* dopuszczenie jedynie zmiennych zadeklarowanych wcześniej na tym samym poziomie,
* dopuszczenie jedynie zmiennych i funkcji zadeklarowanych wcześniej na tym samym poziomie
* dopuszczenie jedynie zmiennych i funkcji zadeklarowanych wcześniej na tym samym poziomie,
* dopuszczenie jedynie zmiennych zadeklarowanych wcześniej na tym samym poziomie i wszystkich funkcji z tego poziomu
* dopuszczenie jedynie zmiennych zadeklarowanych wcześniej na tym samym poziomie i wszystkich funkcji z tego poziomu,
* dopuszczenie zmiennych zadeklarowanych wcześniej na tym samym poziomie i wszystkich funkcji
* dopuszczenie zmiennych zadeklarowanych wcześniej na tym samym poziomie i wszystkich funkcji,
* dopuszczenie wszystkich zmiennych zadeklarowanych wcześniej na tym poziomie oraz wszystkich zmiennych i funkcji zakeklarowanych wyżej
* dopuszczenie wszystkich zmiennych zadeklarowanych wcześniej na tym poziomie oraz wszystkich zmiennych i funkcji zadeklarowanych wyżej,
* oznaczanie zmiennych niezainicjalizowanych i sprawdzanie bycia zainicjalizowaną przy każdym odwołaniu
* oznaczanie zmiennych niezainicjalizowanych i sprawdzanie bycia zainicjalizowaną przy każdym odwołaniu,


Odpowiedz na pytania:
Odpowiedz na pytania:
* Która z możliwości daje największe bezpieczeństwo?
* Która z możliwości daje największe bezpieczeństwo?
* Która ma duży narzut czasu wykonania?
* Która ma duży narzut czasu wykonania?
* Która jest najbardziej elastyczna z punktu widzenia programisty piszącego w Kotku?  
* Która jest najbardziej elastyczna z punktu widzenia programisty piszącego w ''Kotku''?  
* Które z nich pozwalają obejść zabezpieczenia?
* Które z nich pozwalają obejść zabezpieczenia?
* Jakie rozwiązanie zostało przyjęte w języku Java?
* Jakie rozwiązanie zostało przyjęte w języku ''Java''?


Weź pod uwagę jakie zmienne i funkcje są dostępne z wewnątrz funkcji.
Weź pod uwagę jakie zmienne i funkcje są dostępne z wewnątrz funkcji.


== Zadanie 7: Sprawdzanie poprawności rzutowania ==
== Zadanie 5: Generowanie kodu ==


W językach obiektowych często zachodzi potrzeba rzutowania na inną podklasę. Najprostszą metodą sprawdzania poprawności rzutowania obiektów jest sprawdzenie, czy wskaźnik do tablicy metod wirtulanych wskazuje na tablicę dla szukanego typu. Jeśli nie, to musimy sprawdzić również nadklasę naszego obiektu (wskaźnik do tablicy metod wirtualnych nadklasy możemy przechowywać w samej tablicy metod wirtualnych. Takie sprawdzenie wymaga przejścia po nadklasach obiektu i może być czasochłonne.
Napisz ostatni fragment kompilatora - generator kodu. Dla uproszczenia generuj kod od razu na maszynę docelową i pomiń optymalizację. Uprość też maksymalnie alokację rejestrów.  


Zaprojektuj metodę sprawdzającą przynależność obiektu do klasy w stałym czasie.
Możesz sprawdzić jak wygląda przykładowy kod używając istniejącego kompilatora do wygenerowania plików asemblera (np. w ''gcc'' opcja ''-s'' przy kompilacji). Możesz użyć istniejących kompilatorów do wygenerowania kodu dla operacji wejścia/wyjścia ('''read''' i '''write''') oraz dla wygenerowania kodu startu i końca Twojego programu.


Wskazówki:
Napisz programy w ''Kotku'' testowe testujące wszystkie konstrukcje języka i sprawdź ich poprawne działanie po skompilowaniu Twoim kompilatorem.
* załóż skończoną wysokość hierarchii klas
* rozważ dodanie pewnych informacji do każdej klasy


=== Zadanie 5.a: Sprawdzanie poprawności rzutowania ===


Etapy pisania kompilatora
W językach obiektowych często zachodzi potrzeba rzutowania na podklasę obiektu. Najprostszą metodą sprawdzania poprawności rzutowania jest sprawdzenie, czy wskaźnik do tablicy metod wirtualnych wskazuje na tablicę dla szukanego typu. Jeśli nie, to musimy sprawdzić również nadklasę naszego obiektu (wskaźnik do tablicy metod wirtualnych nadklasy możemy przechowywać w samej tablicy metod wirtualnych. Takie sprawdzenie wymaga przejścia po nadklasach obiektu i może być czasochłonne.
# podstawowe konstrukcje: '''for''', '''while''', zmienne tylko typu int i string, proste
operacje arytmetyczne, funkcje globalne, rekurencja (też wzajemna)
# rekordy, tablice,dynamiczna alokacja, zwalnianie jawne ('''delete''')
# lokalne procedury
# klasy deklarowane na poziomie globalnym
# klasy lokalne
# deklaracje zmiennej z podaną wartością do inichalizacji
 
Możliwe rozszerzenia
 


Zaprojektuj metodę sprawdzającą przynależność obiektu do klasy w stałym czasie.


Wskazówki:
* załóż skończoną wysokość hierarchii klas,
* rozważ dodanie pewnych informacji do każdej klasy.


=== Zadanie 5.b: Metody i zmienne statyczne ===


 
Niektóre języki programowania obiektowego (np. ''C++'', ''Java'') dopuszczają statyczne metody i pola klas, a także metody finalne, czyli nie podlegające przedefiniowaniu w podklasach.
== Metody i zmienne statyczne ==
 
Niektóre języki programowania obiektowego (np. C++, java) dopuszczają statyczne pola klas i metody, a także metody finalne, czyli nie podlegające przedefiniowaniu w podklasach.


Czym może różnić się kod wynikowy dla dostępu do statycznych pól, statycznych metod i finalnych metod od normalnego kodu? Jak takie właściwości pomagają w optymalizacji?
Czym może różnić się kod wynikowy dla dostępu do statycznych pól, statycznych metod i finalnych metod od normalnego kodu? Jak takie właściwości pomagają w optymalizacji?


= Rozszerzenia języka =
== Zadanie 6: Rozszerzenia języka ==


== Inicjalizacja zmiennych ==
Język ''Kotek'' w obecnej postaci jest raczej ubogi. Pożądane jest rozszerzenie go. Poniższe rozszerzenia nie muszą być wykonywane w podanej kolejności.


Zmień deklaracje zmiennych tak, żeby przy deklaracji w procedurze, rekordzie lub klasie (ale nie jako parametru funkcji) trzeba było (obowiązkowo!) podać wartość inicjalizacji zmiennej. Przykład:
=== Zadanie 6.a: Typ logiczny ===


'''var''' x : '''int''' := 50;
Dodaj do języka typ logiczny '''bool'''. Popraw analizę semantyczną tak, aby
rozróżniać typy '''int''' i '''bool'''. Rozszerz gramatykę wyrażeń logicznych tak, aby
a || b || c
a && b && c
a && b || c
były poprawnymi wyrażeniami. Załóż priorytet && nad ||.


=== Zadanie 6.b: Typ zmiennoprzecinkowy ===


Przyjmij, że inicjalizacja tablic i rekordów następuje w momencie ich tworzenia ('''new'''):
Dodaj do języka typy zmiennoprzecinkowe: '''float''' (pojedyncza precyzja) i '''double''' (podwójna precyzja). Zmień analizę semantyczną tak, aby uwzględniała nowe typy. Dodaj operator rzutowania na odpowiednie typy arytmetyczne:


  y = '''new''' '''int'''[3*x] of 5;
  ('''int''') Wyrażenie
  z = '''new''' myRecord { field1 := 4, field2 := "ala ma kota"};
  ('''float''') Wyrażenie
...


Przy inicjalizacji zmiennej dopuszczalne jest użycie zmiennej zadeklarowanej wyżej na tym samym poziomie, wszystkich funkcji na tym poziomie oraz wszstkich zmiennych i funkcji zadeklarowanych na poziomach wyższych.
=== Zadanie 6.c: Dodatkowe operatory ===


== Odśmiecanie ==
Dodaj nowe operatory dla wyrażeń, znane z języka ''C'': '''++''', '''--''', '''+=''' (i pokrewne), operatory bitowe ('''|''', '''&''').
Usuń konstrukcję '''delete''' i zrób zamiast tego automatyczne odśmiecanie.


== Ulepszenie read i write ==
=== Zadanie 6.d: Automatyczne odśmiecanie ===


Język Kotek zawiera bardzo prymitywne mechanizmy wejścia wyjścia. Rozszesz język o:
Zrób automatyczne odśmiecanie niedostępnych obiektów (zamiast instrukcji '''delete'''). Najlepiej użyj gotowego odśmiecacza (ang. '''Garbage Collector'''), choć możesz pokusić się o własną implementację
* możliwość odczytania wartości do pola rekordu lub klasy oraz do elementu tablicy (obecnie gramatyka dopuszcza czytanie tylko zmiennej atomowej)
 
'''read''' a[5];
'''read''' mojRekord.pole1;
 
* możliwość czytania i pisania wielu wartości na raz


'''read''' a[i], b[i];
=== Zadanie 6.e: Rzutowanie i sprawdzanie typu obiektu ===
'''write''' "a[5] = ", a[5];


 
Rzutowanie oraz sprawdzanie typu są konstrukcjami często wykorzystywanymi w programowaniu obiektowym.
== Rzutowanie i sprawdzanie typu obiektu ==
Język nie zawiera rzutowania na typy. Jest to konstrukcja bardzo często wykorzystywana w programowaniu obiektowym, gdy trzeba zrzutować obiekt na typ podlkasy. Do tego jest też potrzebne sprawdzanie, czy obiekt jest elementem danej klasy.


Rozszerz język o konstrukcję rzutowania:
Rozszerz język o konstrukcję rzutowania:
Linia 292: Linia 244:
  obiekt '''instance of''' klasa
  obiekt '''instance of''' klasa


Obie konstrukcje są wyrażeniami. Pierwsza daje typ Podklasa a druga typ int (wartosc 0 lub nie-zero)
Obie konstrukcje są wyrażeniami. Pierwsza daje typ ''Podklasa'' a druga typ '''int''' (wartość 0 lub nie-zero; jeśli do języka został dodany typ '''bool''', to preferowany jest typ '''bool'''). Wartością wyrażenia rzutowania jest zero, jeśli obiekt nie jest instancją klasy na którą rzutowanie jest wykonywane (dynamiczne sprawdzanie typów przy rzutowaniu).


=== Zadanie 6.f: Ulepszenie read i write ===


Język ''Kotek'' zawiera bardzo prymitywne mechanizmy wejścia wyjścia. Rozszerz język o:
* możliwość odczytania wartości do pola rekordu lub klasy oraz do elementu tablicy (obecnie gramatyka dopuszcza czytanie tylko zmiennej atomowej)


== Typ bool ==
'''read''' a[5];
'''read''' mojRekord.pole1;


W języku nie ma typu bool. Dodaj ten typ do języka. Popraw analizę semantyczną tak, aby
* możliwość czytania i pisania wielu wartości na raz
rozróżniać typy int i bool. Rozszeż wyrażenia logiczne tak, aby
a || b || c
a && b && c
a && b || c
były poprawnymi wyrażeniami. Załóż priorytet && nad ||.
 
== Typ zmiennoprzecinkowy ==
 
Dodaj do języka typy zmiennnoprzecinkowe: '''float''' (pojedyncza precyzja) i '''double''' (podwójna precyzja). Zmień analizę semantyczną tak, aby uwzględniała nowe typy. Dodaj operator rzutowania na nowe typy.


'''read''' a[i], b[i];
'''write''' "a[5] = ", a[5];


== Chronione pola klasy ==
=== Zadanie 6.g: Chronione pola klasy ===


Dodaj do języka mechanizm ochrony deklaracji klasowych. Deklaracja będzie rozszerzona o modyfikator '''private''', '''public''' i '''protected''' (obok nazwy typu deklaracji):
Dodaj do języka mechanizm ochrony deklaracji klasowych. Deklaracja wewnątrz klasy będzie rozszerzona o modyfikator '''private''', '''public''' lub '''protected''' (obok nazwy typu deklaracji):


  '''var''' '''private''' x : '''int''';
  '''var''' '''private''' x : '''int''';
Linia 320: Linia 269:
Deklaracje '''public''' mają być widoczne zawsze, '''private''' tylko wewnątrz klasy a '''protected''' wewnątrz klasy i w podklasach.
Deklaracje '''public''' mają być widoczne zawsze, '''private''' tylko wewnątrz klasy a '''protected''' wewnątrz klasy i w podklasach.


== Typy dostępne bez deklaracji ==
=== Zadanie 6.h: Typy złożone dostępne bez deklaracji ===


W obecnej postaci język wymaga deklaracji typów złożonych przed ich użyciem, np.
W obecnej postaci język wymaga deklaracji typów złożonych przed ich użyciem, np.
Linia 334: Linia 283:


  '''var''' x : '''array of''' '''array of''' '''int''';
  '''var''' x : '''array of''' '''array of''' '''int''';
Możesz też pokusić się o implementację anonimowych rekordów deklarowanych przy deklaracji zmiennej.
=== Zadanie 6.i: Inicjalizacja zmiennych ===
Zmień deklaracje zmiennych tak, żeby przy deklaracji w procedurze, rekordzie lub klasie (ale nie jako parametru funkcji) trzeba było (obowiązkowo!) podać wartość inicjalizacji zmiennej. Przykład:
'''var''' x : '''int''' := 50;
Przyjmij, że inicjalizacja tablic i rekordów następuje w momencie ich tworzenia ('''new'''):
y = '''new''' '''int'''[3*x] '''of''' 5;
z = '''new''' myRecord { field1 := 4, field2 := "ala ma kota"};
Przy inicjalizacji zmiennej dopuszczalne jest użycie zmiennej zadeklarowanej wcześniej na tym samym poziomie, wszystkich funkcji na tym poziomie oraz wszystkich zmiennych i funkcji zadeklarowanych na poziomach wyższych.
= Wymagania implementacyjne i ocenianie =
Aby zaliczyć laboratorium trzeba napisać działający kompilator. Na niższą ocenę nie musi on kompilować wszystkich konstrukcji języka. Implementacja rozszerzeń języka jest dodatkowo punktowana, ale nie wymagana do zaliczenia przedmiotu.
{| border="1" align=center cellspacing="0"
! Ocena: 
! Wymagania:
|-
| 5
| Wszystkie podstawowe rzeczy (bez rozszerzeń języka) a dodatkowo operator '''instance of''' i rzutowanie (Zadanie 6.e). W szczególności muszą być zaimplementowane obiekty i lokalne deklaracje procedur (nie muszą być dostępne lokalne deklaracje klas).
|-
| 4
| Bez obiektów. Muszą być dostępne lokalne deklaracje procedur.
|-
| 3
| Bez obiektów i lokalnych deklaracji procedur.
|}

Aktualna wersja na dzień 18:41, 30 wrz 2006

Autor: Marek Biskup (mbiskup@mimuw.edu.pl)

Wstęp

Tematem Laboratorium Metod Realizacji Języków Programowania jest zaimplementowanie kompletnego kompilatora języka Kotek.

Język kotek

Kotek jest obiektowym językiem programowania zbliżonym do języków takich jak Pascal czy Java. Podstawowe cechy Kotka to:

  • obecność zwykłych konstrukcji takich jak pętle for i while, wyrażeń arytmetycznych, logicznych,
  • silne typowanie - zgodność typów jest sprawdzana w czasie kompilacji,
  • możliwość deklarowania funkcji,
  • obecność struktur danych takich jak tablice i rekordy,
  • możliwość deklarowania klas i tworzenia obiektów,
  • lokalne deklaracje funkcji i klas,
  • dynamiczna alokacja obiektów, rekordów i tablic (na stercie).

Gramatyka Kotka

Gramatyka języka jest opisana na stronie Gramatyka Kotka

Semantyka Kotka

Semantyka języka jest opisana na stronie Semantyka Kotka

Zadania

Zadanie 0: Wybierz narzędzia i platformę docelową

Wybierz platformę docelową kompilatora języka Kotek. Zalecane są platforma x86 i system operacyjny Linux (kod byłby generowany w asemblerze x86, np. w formacie akceptowanym przez kompilator gcc), bądź Maszyna Wirtualna Javy (kod generowany w asemblerze JVM, np. w formacie akceptowanym przez asembler Jasmin).

Następnie wybierz język programowania, w którym będzie pisany sam kompilator. Ten język nie ma żadnego związku z platformą docelową. Zorientuj się, jakie narzędzia są dostępne dla języka, na który się decydujesz.

  • język C/C++: generator analizatorów leksykalnych lex lub flex, generator parserów yacc lub bison,
  • język Java: generator analizatorów leksykalnych JFlex, generator parserów Coco, CUP, Javacc, ANTLR,
  • język Ocaml: ocamllex, ocamlyacc.

Niektóre narzędzia wspierają też budowanie drzewa składniowego i jego przekształcanie.

Zadanie 1: Analizator leksykalny

Napisz analizator leksykalny do kompilatora języka Kotek. Użyj generatora analizatorów leksykalnych (takich jak lex dla C) generującego kod w języku, w którym będziesz pisał kompilator. Analizator powinien rozpoznawać konstrukcje leksykalne Kotka: identyfikatory, operatory, stałe napisowe, komentarze (jeśli Twój generator analizatorów leksykalnych umożliwia obsługę zagnieżdżonych komentarzy, to powinny one być usuwane już na tym etapie).

Przetestuj swój analizator leksykalny pisząc program, który zamienia ciąg wejściowy na ciąg leksemów, np. dla wejścia:

asdf + qwer * != ( "napis\"napis"

wynikiem działania programu testowego powinno być:

IDENTYFIKATOR("asdf") OPERATOR_PLUS IDENTYFIKATOR("qwer") OPERATOR_RAZY
OPERATOR_RÓŻNE OPERATOR_LNAWIAS, STRING("napis\"napis")

lub równoważny zapis.

Zadanie 2: Analizator składniowy

Napisz parser języka Kotek wykorzystując generator parserów (np. yacc dla C). Wynikiem analizy składniowej ma być drzewo składni, w którym każdy węzeł odpowiada pewnej produkcji w kodzie źródłowym (lewej stronie), dziećmi węzła są natomiast elementy z prawej strony produkcji. Liśćmi są terminale.

Dopuszczalne są modyfikacje drzewa składniowego, które upraszczają jego strukturę, a nie gubią żadnych istotnych informacji. Np. zamiast przechowywania listy deklaracji jako węzła, który zawiera pojedynczą deklarację i drugą listę deklaracji, dopuszczalne (a nawet zalecane) jest używanie standardowych kontenerów języka programowania, w którym kompilator jest pisany.

Jeśli używasz języka obiektowego, zalecane jest stworzenie oddzielnej klasy dla każdego typu produkcji. Obiekty należy w miarę możliwości i potrzeb powiązać związkami dziedziczenia. Zwykle dobrym pomysłem jest stworzenie oddzielnej klasy dla lewej strony produkcji i podklas dziedziczących z niej dla każdej prawej strony produkcji, w której ta lewa strona występuje. Na przykład możemy mieć klasę Instrukcja, z której dziedziczą m.in. klasy Przypisanie, Blok, PustaInstrukcja i ZłożonaInstrukcja. Z kolei z klasy ZłożonaInstrukcja będą dziedziczyć klasy InstrukcjaIfElse, InstrukcjaIf oraz InstrukcjaWhile.

Hierarchia klas węzłów drzewa składniowego powinna mieć wspólny korzeń (nazwany np. Węzeł). Ten węzeł będzie zawierał abstrakcyjne metody, które są wymagane w innych węzłach, np. metoda gen(), generująca kod na maszynę docelową, metoda sem() przeprowadzająca analizę semantyczną (uwaga: jeśli analiza semantyczna będzie robiona w kilku przebiegach, to warto stworzyć więcej metod sem, np. sem1 i sem2).

Napisz program testujący parser. Program testujący powinien działać jak pretty printer, tzn. wypisywać treść programu na podstawie drzewa składniowego zawartego w pamięci.

Zadanie 2.a: Poprawność wyrażeń na etapie parsowania

Gramatyka języka dopuszcza pewne wyrażenia, które nie są poprawne, np:

funkcja(param)(param2)
zmienna[indeks](param)

Niepoprawność wynika z tego, że funkcja nie może być wynikiem innej funkcji, funkcja nie może też być przechowywana w tablicy. Czy można zmienić gramatykę tak, aby wykluczała podane wyrażenia? Jakie podejście preferujesz: sprawdzanie poprawności tych wyrażeń na etapie parsera czy analizy semantycznej? Podaj zalety i wady obu metod.

Zadanie 2.b: Poprawność przypisań

Obecna postać produkcji przypisania:

Instrukcja ::= WyrazeniePostfiksowe ":=" Wyrazenie ";";

dopuszcza pewne błędne przypisania, np.

"tekst" := 8;

Czy można tak zmienić gramatykę, aby przypisanie zawsze było poprawne? Czy pozwoli to wyeliminować analizę semantyczną tego fragmentu kodu źródłowego?

Zadanie 3: Tablica symboli

Do Twojego kompilatora dopisz kod tworzący tablicę symboli z każdej listy deklaracji. Połącz tablice symboli w graf zgodnie z regułami widoczności nazw. Tablica symboli powinna mieć odniesienie do tablicy symboli nadrzędnej funkcji lub klasy, w której nasza funkcja (lub klasa) została zadeklarowana. W przypadku tablicy symboli klasy dodaj jeszcze odnośniki do tablicy symboli nadklasy. Określ kolejność szukania nazw (najpierw nadklasa, czy nadfunkcja?).

Dla każdej deklaracji określ rozmiar zużytej pamięci, np. w 32 bitowej architekturze x86 zmienne typu int zużywają 4 bajty, zmienne string również 4 bajty (napis jest reprezentowany przez wskaźnik do ciągu znaków zakończonego zerem) i tak samo zmienne typu obiektowego, tablicowego i rekordowego. Deklaracje funkcji, rekordów, typów i klas nie zajmują żadnego miejsca (w Kotku nie są z nimi skojarzone żadne modyfikowalne wartości). Oczywiście rozmiar pamięci zależy od docelowej architektury. Dla każdej kolejnej deklaracji określ jej położenie w pamięci względem pierwszego elementu. Na przykład na wspomnianej wcześniej architekturze dla deklaracji:

var x : int;
var y : string;
function f() : void {return;}
var z : int;

położenie zmiennej x to 0, zmiennej y to 4, funkcji f to 4 (choć ta wartość nie będzie do niczego potrzebna i można ją opuścić), a zmiennej z to 8.

Tablica symboli powinna udostępniać następujące funkcje:

  • znajdowanie identyfikatora - określenie, w której tablicy symboli jest zawarty,
  • określanie pozycji identyfikatora w tablicy symboli,
  • podawanie całkowitego rozmiaru wszystkich deklaracji z tablicy symboli.

Rozbuduj pretty printer z poprzedniego zadania tak, aby obok każdego identyfikatora wypisywał, z której tablicy symboli identyfikator pochodzi i jakie jest jego przesunięcie w tej tablicy symboli. Nie musisz na razie uwzględniać operatora kropki (odwołanie do pól obiektu). Określ sam czytelny format danych wyjściowych.

Przetestuj swój kod na trzech przykładach. Przykłady powinny zawierać wszystkie następujące konstrukcje: dziedziczenie, funkcje lokalne, zmienne lokalne, przysłanianie zmiennej, argumenty funkcji, obiekty lokalne wewnątrz funkcji i innych obiektów, rekordy.

Zadanie 4: Analiza semantyczna

Do swojego kompilatora dopisz kod odpowiedzialny za analizę semantyczną. Głównym zadaniem analizy semantycznej jest określenie typu każdego węzła (niektóre konstrukcje nie mają typu; wtedy typem węzła będzie void) oraz sprawdzanie poprawności typów. Dodatkowo musisz sprawdzać wszystkie inne cechy semantyczne 'Kotka'. Opracowanie kompletnej listy wszystkich rzeczy, które należy sprawdzać jest skomplikowane, więc nie przejmuj się, jeśli o czymś zapomnisz. Zawsze możesz uzupełnić kod analizy semantycznej w czasie pisania generatora kodu. Zajrzyj do zadań z tego rozdziału - być może pomogą Ci lepiej zrozumieć zawiłości semantyki języków programowania.

Na początku przyjmij, że nie jest dozwolona deklaracja klas wewnątrz innych klas i procedur. Następnie, rozwiązując poniższe podzadania, zastanów się jakie ograniczenia należy wprowadzić, aby definiowanie klas wewnątrz klas i procedur miało sens.

Rozszerz program testowy kompilatora (który po poprzednim zadaniu wypisuje tablicę symboli, z której pochodzi każdy symbol wraz z przesunięciem) o wypisywane typu każdego identyfikatora. Dopisz obsługę rekordów i obiektów (operator kropki).

Zadanie 4.a: Klasy wewnętrzne procedur

W tym zadaniu rozważamy jedynie procedury, które nie są metodami klasy ani nie są zawarte z jakiejś metodzie.

Klasy zadeklarowane lokalnie wewnątrz procedur przydają się do zmniejszenia liczby globalnych nazw oraz do ścisłego określenia zakresu użycia klasy. Udostępnienie takich konstrukcji prowadzi do dodatkowych problemów implementacyjnych podobnych do problemów z zagnieżdżonymi procedurami.

Pytania:

  • Podaj przykład programu, w którym występuje funkcja zwracająca instancję lokalnie zadeklarowanej klasy.
  • Czy do zewnętrznych zmiennych można odwoływać się za pomocą tablicy display, jak w przypadku procedur?
  • Język Java dopuszcza korzystanie z lokalnych zmiennych procedury, ale tylko zmiennych zadeklarowanych jako final. Dlaczego takie rozwiązanie upraszcza implementację?
  • Czy dostęp do zmiennych globalnych utrudnia implementację?
  • Jakie jest możliwe rozwiązanie ogólnego przypadku? Czy widzisz związek z językami funkcyjnymi?

Zadanie 4.b: Klasy wewnątrz klas

Kolejnym krokiem do zwiększenia modularności jest dopuszczenie deklaracji klasy wewnątrz klas (na razie nie wewnątrz metody klasy). Taka klasa wewnętrzna miałaby dostęp do metod i zmiennych klasy, wewnątrz której jest zadeklarowana.

  • Zaproponuj sposób implementacji dla takich klas.
  • Kiedy jest możliwe utworzenie instancji klasy wewnętrznej bez klasy zewnętrznej?

Zadanie 4.c: Dowolne, lokalne deklaracje klas

Zastanów się nad sposobem implementacji dowolnie zagnieżdżonych deklaracji klas. To zadanie jest połączeniem dwóch poprzednich.

  • Jakie informacje musi nieść ze sobą klasa zadeklarowana wewnątrz innej klasy (być może wewnątrz jakiejś metody tamtej klasy lub wewnątrz funkcji zdefiniowanej w metodzie)?
  • Jakie informacje musi nieść ze sobą klasa zadeklarowana wewnątrz pewnej funkcji (być może funkcja jest metodą innej klasy)?

Zadanie 4.d: Inicjalizacja zmiennych

Aby język był bardziej bezpieczny chcielibyśmy wyeliminować możliwość użycia niezainicjalizowanych zmiennych. W tym celu modyfikujemy deklaracje zmiennej tak, aby podanie wartości przy deklaracji było obowiązkowe:

var x : int = 5;
var y : int = 3*x+5;

a nie

var x : int;     // zle! - brak wartości
var y : int;     // zle! - brak wartości

Zastanów się, jakie ograniczenia należy wprowadzić na wyrażenia użyte do inicjalizacji aby język był całkowicie bezpieczny. Na przykład poniższa sekwencja deklaracji prowadzi do błędu:

var x : int = y + 4;
var y : int = x + 4;

Przemyśl następujące możliwości:

  • dopuszczenie tylko wyrażeń stałych,
  • dopuszczenie jedynie zmiennych zadeklarowanych wcześniej na tym samym poziomie,
  • dopuszczenie jedynie zmiennych i funkcji zadeklarowanych wcześniej na tym samym poziomie,
  • dopuszczenie jedynie zmiennych zadeklarowanych wcześniej na tym samym poziomie i wszystkich funkcji z tego poziomu,
  • dopuszczenie zmiennych zadeklarowanych wcześniej na tym samym poziomie i wszystkich funkcji,
  • dopuszczenie wszystkich zmiennych zadeklarowanych wcześniej na tym poziomie oraz wszystkich zmiennych i funkcji zadeklarowanych wyżej,
  • oznaczanie zmiennych niezainicjalizowanych i sprawdzanie bycia zainicjalizowaną przy każdym odwołaniu,

Odpowiedz na pytania:

  • Która z możliwości daje największe bezpieczeństwo?
  • Która ma duży narzut czasu wykonania?
  • Która jest najbardziej elastyczna z punktu widzenia programisty piszącego w Kotku?
  • Które z nich pozwalają obejść zabezpieczenia?
  • Jakie rozwiązanie zostało przyjęte w języku Java?

Weź pod uwagę jakie zmienne i funkcje są dostępne z wewnątrz funkcji.

Zadanie 5: Generowanie kodu

Napisz ostatni fragment kompilatora - generator kodu. Dla uproszczenia generuj kod od razu na maszynę docelową i pomiń optymalizację. Uprość też maksymalnie alokację rejestrów.

Możesz sprawdzić jak wygląda przykładowy kod używając istniejącego kompilatora do wygenerowania plików asemblera (np. w gcc opcja -s przy kompilacji). Możesz użyć istniejących kompilatorów do wygenerowania kodu dla operacji wejścia/wyjścia (read i write) oraz dla wygenerowania kodu startu i końca Twojego programu.

Napisz programy w Kotku testowe testujące wszystkie konstrukcje języka i sprawdź ich poprawne działanie po skompilowaniu Twoim kompilatorem.

Zadanie 5.a: Sprawdzanie poprawności rzutowania

W językach obiektowych często zachodzi potrzeba rzutowania na podklasę obiektu. Najprostszą metodą sprawdzania poprawności rzutowania jest sprawdzenie, czy wskaźnik do tablicy metod wirtualnych wskazuje na tablicę dla szukanego typu. Jeśli nie, to musimy sprawdzić również nadklasę naszego obiektu (wskaźnik do tablicy metod wirtualnych nadklasy możemy przechowywać w samej tablicy metod wirtualnych. Takie sprawdzenie wymaga przejścia po nadklasach obiektu i może być czasochłonne.

Zaprojektuj metodę sprawdzającą przynależność obiektu do klasy w stałym czasie.

Wskazówki:

  • załóż skończoną wysokość hierarchii klas,
  • rozważ dodanie pewnych informacji do każdej klasy.

Zadanie 5.b: Metody i zmienne statyczne

Niektóre języki programowania obiektowego (np. C++, Java) dopuszczają statyczne metody i pola klas, a także metody finalne, czyli nie podlegające przedefiniowaniu w podklasach.

Czym może różnić się kod wynikowy dla dostępu do statycznych pól, statycznych metod i finalnych metod od normalnego kodu? Jak takie właściwości pomagają w optymalizacji?

Zadanie 6: Rozszerzenia języka

Język Kotek w obecnej postaci jest raczej ubogi. Pożądane jest rozszerzenie go. Poniższe rozszerzenia nie muszą być wykonywane w podanej kolejności.

Zadanie 6.a: Typ logiczny

Dodaj do języka typ logiczny bool. Popraw analizę semantyczną tak, aby rozróżniać typy int i bool. Rozszerz gramatykę wyrażeń logicznych tak, aby

a || b || c
a && b && c
a && b || c

były poprawnymi wyrażeniami. Załóż priorytet && nad ||.

Zadanie 6.b: Typ zmiennoprzecinkowy

Dodaj do języka typy zmiennoprzecinkowe: float (pojedyncza precyzja) i double (podwójna precyzja). Zmień analizę semantyczną tak, aby uwzględniała nowe typy. Dodaj operator rzutowania na odpowiednie typy arytmetyczne:

(int) Wyrażenie
(float) Wyrażenie
...

Zadanie 6.c: Dodatkowe operatory

Dodaj nowe operatory dla wyrażeń, znane z języka C: ++, --, += (i pokrewne), operatory bitowe (|, &).

Zadanie 6.d: Automatyczne odśmiecanie

Zrób automatyczne odśmiecanie niedostępnych obiektów (zamiast instrukcji delete). Najlepiej użyj gotowego odśmiecacza (ang. Garbage Collector), choć możesz pokusić się o własną implementację

Zadanie 6.e: Rzutowanie i sprawdzanie typu obiektu

Rzutowanie oraz sprawdzanie typu są konstrukcjami często wykorzystywanymi w programowaniu obiektowym.

Rozszerz język o konstrukcję rzutowania:

(Podklasa) mojObiekt

i sprawdzania typu:

obiekt instance of klasa

Obie konstrukcje są wyrażeniami. Pierwsza daje typ Podklasa a druga typ int (wartość 0 lub nie-zero; jeśli do języka został dodany typ bool, to preferowany jest typ bool). Wartością wyrażenia rzutowania jest zero, jeśli obiekt nie jest instancją klasy na którą rzutowanie jest wykonywane (dynamiczne sprawdzanie typów przy rzutowaniu).

Zadanie 6.f: Ulepszenie read i write

Język Kotek zawiera bardzo prymitywne mechanizmy wejścia wyjścia. Rozszerz język o:

  • możliwość odczytania wartości do pola rekordu lub klasy oraz do elementu tablicy (obecnie gramatyka dopuszcza czytanie tylko zmiennej atomowej)
read a[5];
read mojRekord.pole1;
  • możliwość czytania i pisania wielu wartości na raz
read a[i], b[i];
write "a[5] = ", a[5];

Zadanie 6.g: Chronione pola klasy

Dodaj do języka mechanizm ochrony deklaracji klasowych. Deklaracja wewnątrz klasy będzie rozszerzona o modyfikator private, public lub protected (obok nazwy typu deklaracji):

var private x : int;
function public f() : bool;
type protected typ = { var x: int; var y : int };

Deklaracje public mają być widoczne zawsze, private tylko wewnątrz klasy a protected wewnątrz klasy i w podklasach.

Zadanie 6.h: Typy złożone dostępne bez deklaracji

W obecnej postaci język wymaga deklaracji typów złożonych przed ich użyciem, np.

type intArr = array of int;
var x : intArr;

zamiast

var x : array of int;

Rozszerz język o tę nową konstrukcję. Zwróć uwagę, że będzie możliwe napisanie:

var x : array of array of int;

Możesz też pokusić się o implementację anonimowych rekordów deklarowanych przy deklaracji zmiennej.

Zadanie 6.i: Inicjalizacja zmiennych

Zmień deklaracje zmiennych tak, żeby przy deklaracji w procedurze, rekordzie lub klasie (ale nie jako parametru funkcji) trzeba było (obowiązkowo!) podać wartość inicjalizacji zmiennej. Przykład:

var x : int := 50;

Przyjmij, że inicjalizacja tablic i rekordów następuje w momencie ich tworzenia (new):

y = new int[3*x] of 5;
z = new myRecord { field1 := 4, field2 := "ala ma kota"};

Przy inicjalizacji zmiennej dopuszczalne jest użycie zmiennej zadeklarowanej wcześniej na tym samym poziomie, wszystkich funkcji na tym poziomie oraz wszystkich zmiennych i funkcji zadeklarowanych na poziomach wyższych.

Wymagania implementacyjne i ocenianie

Aby zaliczyć laboratorium trzeba napisać działający kompilator. Na niższą ocenę nie musi on kompilować wszystkich konstrukcji języka. Implementacja rozszerzeń języka jest dodatkowo punktowana, ale nie wymagana do zaliczenia przedmiotu.

Ocena: Wymagania:
5 Wszystkie podstawowe rzeczy (bez rozszerzeń języka) a dodatkowo operator instance of i rzutowanie (Zadanie 6.e). W szczególności muszą być zaimplementowane obiekty i lokalne deklaracje procedur (nie muszą być dostępne lokalne deklaracje klas).
4 Bez obiektów. Muszą być dostępne lokalne deklaracje procedur.
3 Bez obiektów i lokalnych deklaracji procedur.