Usprawnienie programowania z użyciem typów w Glasgow Haskell
Transkrypt
Usprawnienie programowania z użyciem typów w Glasgow Haskell
Politechnika Łódzka Wydział Fizyki Technicznej, Informatyki i Matematyki Stosowanej Instytut Informatyki Michał Sośnicki nr albumu 180692 Usprawnienie programowania z użyciem typów w Glasgow Haskell Compiler Praca inżynierska napisana pod kierunkiem dr inż. Jana Stolarka Łódź 2016 ii Spis treści Spis treści iii 1 Wstęp 1.1 Cele pracy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Przegląd literatury . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Układ pracy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2 Budowa kompilatorów i systemy typów 2.1 Budowa kompilatora . . . . . . . . . . 2.1.1 Parser . . . . . . . . . . . . . . 2.1.2 Renamer . . . . . . . . . . . . . 2.1.3 Type checker . . . . . . . . . . 2.2 Rozszerzenia Glasgow Haskell Compiler 2.2.1 Rodziny typów . . . . . . . . . 2.2.2 Częściowe sygnatury typów . . 3 Użyte narzędzia i technologie 3.1 Język programowania . . . . 3.2 Narzędzia . . . . . . . . . . 3.2.1 Git . . . . . . . . . . 3.2.2 Trac . . . . . . . . . 3.2.3 Phabricator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 Usprawnienia Glasgow Haskell Compiler 4.1 Zgłoszenie nr 10839 . . . . . . . . . . . . 4.1.1 Rozwiązanie . . . . . . . . . . . . 4.1.2 Testy . . . . . . . . . . . . . . . . 4.2 Zgłoszenie nr 10982 . . . . . . . . . . . . 4.2.1 Rozwiązanie . . . . . . . . . . . . 4.2.2 Testy . . . . . . . . . . . . . . . . 4.3 Zgłoszenie nr 11098 . . . . . . . . . . . . 4.3.1 Rozwiązanie . . . . . . . . . . . . 4.3.2 Testy . . . . . . . . . . . . . . . . iii . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1 2 3 3 . . . . . . . 5 5 7 7 10 13 13 15 . . . . . 19 19 19 19 20 21 . . . . . . . . . 23 23 23 26 27 28 29 30 31 32 SPIS TREŚCI SPIS TREŚCI 5 Podsumowanie i wnioski 33 5.1 Dyskusja wyników . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 5.2 Perspektywy dalszych badań . . . . . . . . . . . . . . . . . . . . . . 33 Bibliografia 35 Spis rysunków 39 Spis fragmentów programów 41 A Płyta CD 43 iv Rozdział 1 Wstęp Zakresem niniejszej pracy inżynierskiej jest tworzenie kompilatorów dla języków funkcyjnych ze statycznymi systemami typów. Przedmiotem pracy jest usprawnienie programowania z użyciem typów w Glasgow Haskell Compiler. Temat został wybrany ze względu na chęć zapoznania się z budową kompilatora oraz udziału w rozwoju powszechnie używanego programu. W popularnych, statycznie typowanych językach programowania, gramatyka przewiduje kilka rodzajów wyrażeń. Są to termy, służące do wyrażania obliczeń, które mają zostać wykonane po uruchomianiu programu oraz typy, które przypisuje się termom, by rozróżnić termy poprawne oraz niepoprawne. Niektóre języki, w tym Haskell, zawierają jeszcze rodzaje (kinds), które pełnią wobec typów taką rolę, jak typy wobec termów. Zawarte w temacie pracy mechanizmy programowania z użyciem typów dodają do typów, obok ich pierwotnej roli, możliwości wyrażania obliczeń, jakie zwykle zarezerwowana są dla termów. Obliczenia te wykonywane są na etapie sprawdzania typów w czasie kompilacji. Praca została poświęcona językowi Haskell i kompilatorowi Glasgow Haskell Compiler. Wybór ten został podyktowany tym, iż jest to projekt open source i dołączenie do niego ułatwione jest dzięki zasobom opisującym jak się wdrożyć oraz pomocnej społeczności. Drugim kryterium była dostępność rozszerzeń umożliwiających programowanie z użyciem typów. GHC oferuje między innymi uogólnione algebraiczne typy danych (GADTs[8][19]), promocję typów danych (data type promotion[27]), zależności funkcyjne między parametrami w klasach typów (functional dependencies in multi-parameter type classes[14]) i rodziny typów (type families[6][7][9]) Szczególnie istotnym rozszerzeniem są rodziny typów, gdyż umożliwiają definiowanie na poziomie typów funkcji wykorzystujących dopasowywanie wzorców oraz zależności funkcyjne w klasach typów, które dają równoważne możliwości, lecz wymagają programowania w stylu relacyjnym zamiast funkcyjnego. Biblioteka standardowa GHC dostarcza również pomocne moduły, na przykład Data.Type, Data.Proxy i GHC.TypeLits, z przydatnymi typami i funkcjami na poziomie typów, jak operatory logiczne. Programowanie z użyciem typów możliwe jest również w innych językach. Na przykład, Scala oferuje do tego path dependent types, a w C++ możliwe jest metaprogramowanie z użyciem szablonów. Istnieją też języki z typami zależnymi 1 ROZDZIAŁ 1. WSTĘP jak Agda lub Idris. Wiele z tych języków ma implementacje o otwartych źródłach i mogłoby służyć do realizacji pracy o tym samym zakresie zamiast Haskella. Istnieje kilka implementacji języka Haskell. Spośród nich tylko dwie są zgodne z aktualną specyfikacją języka Haskell 2010, Glasgow Haskell Compiler i Utrecht Haskell Compiler[12]. Porównanie repozytoriów pozwala stwierdzić, że spośród nich GHC jest bardziej rozwinięty. Różnica ta objawia się w tym, iż rozszerzenia kompilatora pozwalające na programowanie na poziomie typów jak rodziny typów istnieją wyłącznie w GHC[25]. Z tego powodu w pracy został wybrany ten kompilator. Rodziny typów zostały wprowadzone do Haskella w wersji 6.10.1, czyli w roku 2008[10]. Do tej pory są przedmiotem prac naukowych, a ich implementacja jest nieustannie rozwijana. Na przykład, w roku 2015 zostały wprowadzone do GHC różnowartościowe rodziny typów[13]. Aktywny rozwój świadczy o tym, iż jest to temat bardzo aktualny. Czynnik ten sprawia też, iż wiele usprawnień do nowo dodanych funkcji, ułatwiających korzystanie z nich, wciąż czeka na realizację. Istotny jest też fakt, iż type checker to największy komponent GHC[16], więc podczas modyfikacji z dużym prawdopodobieństwem powstają nowe błędy. Podjęcie się realizacji tych usprawnień i naprawienia błędów są zatem dobrym tematem na pracę. 1.1 Cele pracy Celem pracy jest dokonanie jak największej liczby usprawnień w kompilatorze GHC. Przez usprawnienia rozumiane są zmiany w GHC lub w powiązanych z tym projektem bibliotekach. Zmiany w GHC zgłaszane są w systemie Trac, który używany jest w projekcie do organizacji pracy programistów. Jako cele pracy wybranych zostało kilka zgłoszeń spośród tych znajdujących się w systemie Trac. Podjęte zostały wyłącznie usprawnienia polegające na zmianach w kodzie, w częściach związanych z rozszerzeniami opisanymi wcześniej. Wybrane usprawnienia to: • Ujednolicenie wyświetlania rodzin typów w błędach i ostrzeżeniach kompilatora. • Dodanie ostrzeżeń o nieużywanych zmiennych w rodzinach typów. • Naprawienie błędu z niewłaściwym parsowaniem zmiennych zaczynających się od podkreślnika w przypadku kompilacji z rozszerzeniem NamedWildCards. Poprawne wprowadzenie zmiany w kodzie wymaga przejścia przez procedurę ustaloną przez osoby z prawem zapisu do repozytorium[26]. Konieczne jest zatem pisanie kodu akceptowalnej jakości, zgodnego z ustalonymi konwencjami. Zgodnie z procedurą, wykonanie usprawnienia wymaga: • Informowania o stanie pracy w systemie Trac. Oznaczenie swojego konta jako właściciela zadania, uzupełnienie informacji na przykład o testach i powiązanych zadaniach, a na koniec oznaczenie zgłoszenia jako zrealizowanego. 2 ROZDZIAŁ 1. WSTĘP • Przygotowania testów pokrywających wprowadzone zmiany. Dodanie ich do zestawu testów wykonywanych w czasie walidacji. • Dokonanie zmian w kodzie w zgodzie z obowiązującymi konwencjami wraz z dokumentacją. Sporządzenie z nich commitów w systemie kontroli wersji git. • Wysłanie łatki do systemu Phabricator, gdzie przejdzie ona inspekcję. Jeżeli nie będzie ku temu przeszkód, zmiany zostaną przeniesione do głównego repozytorium. 1.2 Przegląd literatury Podstaw teorii budowy kompilatorów dostarcza [1]. Autorzy opisują budowę kompilatora, a następnie każdą fazę kompilacji. Ma to zastosowanie także w przypadku kompilatora GHC. Krótki opis poświęcony konkretnie GHC dostępny jest w [16]. Podstawy teorii typów zawarte są w [21]. Omówiony jest w tej pozycji nietypowany i typowany rachunek lambda i kilka jego wzbogaconych wersji. To bardzo istotne, gdyż pojęcia z rachunku lambda występują powszechnie w dokumentacji, w kodzie GHC i są powszechnie używane w dyskusjach przez programistów. Z drugiej strony część poświęcona podtypowaniu nie ma bezpośredniego zastosowania do GHC oraz opis nie obejmuje typów zależnych. Szczególnie warte uwagi w ramach wprowadzenia teoretycznego są również dwa kursy, dostępne za darmo na platformie Coursera: Automata prowadzony przez Jeffa Ullmana oraz Programming Languages prowadzony przez Dana Grossmana. Są to zaadoptowane do formy kursu internetowego kursy akademickie. Praca nad GHC wymaga znajomości Haskella, w którym jest tworzony kompilator. [15] i [23] stanowią dobre wprowadzenie do tego języka, obie pozycje są dostępne za darmo w Internecie. Z rozszerzeniami zmienionymi w tej pracy, TypeFamilies i PartialTypeSignatures można zapoznać się w poradniku użytkownika GHC, w zasobach [24] i [18]. Strona systemu Trac projektu GHC zawiera wiele zasobów pozwalających zainteresowanym programistom wdrożyć się w projekt. Jest tam między innymi opis jak pobrać i skompilować źródła GHC[17], opis procedury wprowadzania zmian w kodzie[26] oraz instrukcja konfiguracji i wykorzystania narzędzia Phabricator[20]. 1.3 Układ pracy Rozdział 1 zawiera wprowadzenie i określenie tematu oraz celu pracy. Rozdział 2 zawiera opis teorii budowy kompilatorów i systemów typów związanej z tematem. Rozdział 3 opisuje technologie i narzędzia wykorzystane w pracy. W rozdziale 4 przedstawiono opis dokonanych zmian w GHC. Rozdział 5 zawiera podsumowanie czy założone cele zostały osiągnięte. W dodatku A znajduje się płyta CD z ko3 ROZDZIAŁ 1. WSTĘP dem aplikacji, wersją elektroniczną tej pracy oraz kopią wykorzystanych źródeł internetowych. 4 Rozdział 2 Budowa kompilatorów i systemy typów 2.1 Budowa kompilatora Kompilator to program, który przyjmuje na wejściu reprezentację programu w pewnym języku źródłowym i zwraca na wyjściu równoważny program w innym języku. Językiem docelowym często bywa kod wykonywalny, bajtkod, ale może to być też na przykład C lub JavaScript. Pod względem architektury w kompilatorach można wyróżnić frontend, dokonujący przetworzenia programu wejściowego i backend, generujący kod docelowy. Podział ten umożliwia utrzymywanie wielu frontendów i backendów, na przykład w GHC dostępne są trzy backendy, generujące kod maszynowy, kod w języku pośrednim LLVM lub kod w C[16]. GHC może być uruchomiony w trybie interaktywnym, jako interpreter GHCi. Frontend odpowiada za przeanalizowanie tekstu otrzymanego na wejściu zgodnie z gramatyką języka, zbudowanie z niego drzewa składniowego i przeprowadzenie statycznej kontroli typów. W razie błędów konieczne jest wyświetlenie użytkownikowi wiadomości z opisem wystarczającym do jego zrozumienia. Następnie następuje faza generowania kodu pośredniego. Język, w którym program pisany jest przez użytkownika, ma zwykle rozbudowaną składnię i jest zaprojektowany z myślą o łatwym użytkowaniu przez ludzi. Języki pośrednie są z kolei możliwie uproszczone pod względem składni i zaprojektowane z myślą o dalszym przetwarzaniu przez kompilator. Kod ten przekazywany jest do backendu, gdzie wykonywane są na nim optymalizacje. Możliwe jest, że ostatnie dwie fazy powtórzą się wielokrotnie, czyli program przejdzie przez kilka języków pośrednich i faz optymalizacji. Na koniec następuje wygenerowanie kodu w języku docelowym[1]. Glasgow Haskell Compiler ma budowę wpisującą się w ten krótki, ogólny opis. Na schemacie 2.1 można odnaleźć wymienione wcześniej fazy. W tej pracy dokonano modyfikacji we frontendzie, dlatego poświęcono mu więcej uwagi. 5 ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW Rysunek 2.1: Schemat Glasgow Haskell Compiler[16] 6 ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW 2.1.1 Parser Parsowanie w GHC dzieli się na etapy analizy leksykalnej i składniowej. Jest to proces tak dobrze opracowany od strony algorytmicznej, że istnieją narzędzia, które pozwalają na generowanie gotowych lekserów i parserów z opisu gramatyki. W czasie analizy leksykalnej plik na wejściu jest skanowany, dzielony na ciągi znaków, tzw. leksemy i generowany jest z niego strumień wartości zwanych tokenami. W GHC tokeny to wartości typu Token, zdefiniowanego w pliku Lexer.x. Gramatyka leksykalna składa się z szeregu wyrażeń regularnych i odpowiadających im tokenów. Leksemy wyodrębniane są, gdy dojdzie do dopasowania ciągu znaków na wejściu do jednego z wyrażeń. Stosowana jest metoda najdłuższego dopasowania, to znaczy lekser wczytuje kolejne znaki tak długo, dopóki przynajmniej jedno wyrażenie akceptuje wczytany podciąg. Po dopasowaniu lekser zwraca token odpowiadający temu wyrażeniu. Proces ten jest kontynuowany do przetworzenia całego wejścia. W GHC lekser jest generowany z wykorzystaniem narzędzia Alex. Definicja gramatyki leksykalnej zawarta jest w pliku Lexer.x. W przykładzie 2.1 pokazane są fragmenty tego pliku. Na początku widoczne są makra, za którymi stoją wyrażenia regularne. Na końcu fragmentu pokazane jest kilka reguł z tymi makrami po lewej stronie i wyrażeniami, które mają zostać zwrócone po dopasowaniu do danego wyrażenia regularnego. Znajdujące się tam idtoken, varid itd. to zwykłe funkcje w Haskellu, zdefiniowane w innym miejscu, które zwracają tokeny[2]. W czasie analizy składniowej strumień tokenów zamieniany jest w bardziej złożoną strukturę, drzewo składniowe. W GHC parser, tak jak lekser, został wygenerowany automatycznie. Wykorzystano do tego narzędzie Happy. Definicja gramatyki w formacie akceptowanym przez Happy znajduje się w pliku Parser.y. Jest podana w notacji Backusa-Naura wzbogaconej, podobnie jak w Alex, o fragmenty kodu w Haskellu wywoływane w celu stworzenia struktury danych reprezentującej to drzewo[11]. Przykład 2.2 pokazuje wybrane produkcje z tego pliku, wykorzystujące przy tym token ITvarid zwracany przez funkcję varid z przykładu 2.1. 2.1.2 Renamer W Haskellu, jak w wielu innych językach, może istnieć kilka funkcji, zmiennych, typów danych, klas, itd. o tej samej nazwie. Na przykład, mogą one występować w różnych plikach albo w lokalnym zakresie w konstrukcjach let lub where. Z tego powodu później ten sam identyfikator może odnosić się do różnych rzeczy, w zależności od tego, gdzie występuje i co w tym miejscu jest widoczne w zakresie. Kompilator, w części zwanej renamerem, decyduje, do której definicji odnosi się każde wystąpienie danej nazwy. Każda osobna definicja otrzymuje na tym etapie unikalny, wewnętrzny identyfikator. Następnie nazwy w programie są zamieniane na odniesienia do jednej z tych zmiennych. Utrzymywany jest w tym celu słownik nazw znajdujących się w danej chwili w zakresie. Jeżeli w danym miejscu nie da się na podstawie zawartości słownika jednoznacznie określić, do czego odnosi się pewne wystąpienie nazwy, to 7 ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW Fragment 2.1: Wycinki z pliku Lexer.x składające się na reguły opisujące co jest wyodrębniane jako zmienna i konstruktor. $unilarge $asclarge $large = \ x01 -- Trick Alex into h a n d l i n g Unicode . See a l e x G e t B y t e . = [A - Z ] = [ $asclarge $unilarge ] $unismall $ascsmall $small = \ x02 -- Trick Alex into h a n d l i n g Unicode . See a l e x G e t B y t e . = [a - z ] = [ $ascsmall $unismall \ _ ] $ascdigit $unidigit $digit = 0 -9 = \ x03 -- Trick Alex into h a n d l i n g Unicode . See a l e x G e t B y t e . = [ $ascdigit $unidigit ] $digit $suffix $idchar = [ $ascdigit $unidigit ] = \ x07 -- Trick Alex into h a n d l i n g Unicode . See a l e x G e t B y t e . = [ $small $large $digit $suffix \ ’] @varid @conid = $small $idchar * = $large $idchar * -- v a r i a b l e i d e n t i f i e r s -- c o n s t r u c t o r i d e n t i f i e r s @qual = ( @conid \.) + @qvarid = @qual @varid @qconid = @qual @conid haskell : <0 , option_prags > { @qvarid @qconid @varid @conid } { { { { idtoken qvarid } idtoken qconid } varid } idtoken conid } Fragment 2.2: Wycinki z pliku Parser.y z produkcjami odpowiadającymi za zmienne typów, wykorzystujące tokeny, których dotyczył przykład 2.1. % token VARID CONID tyvar tyvar { L _ ( ITvarid { L _ ( ITconid _) } _) } :: { Located RdrName } : tyvarid { $1 } tyvarid :: { Located RdrName : VARID { | special_id { | ’ unsafe ’ { | ’ safe ’ { | ’ interruptible ’ { } sL1 sL1 sL1 sL1 sL1 $1 $1 $1 $1 $1 $! $! $! $! $! -- i d e n t i f i e r s mkUnqual mkUnqual mkUnqual mkUnqual mkUnqual 8 tvName tvName tvName tvName tvName ( getVARID $1 ) } ( unLoc $1 ) } ( fsLit " unsafe " ) } ( fsLit " safe " ) } ( fsLit " interruptible " ) } ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW Fragment 2.3: Przykład działania renamera - niejednoznaczne odwołanie. import Data . Maybe ( isJust ) isJust :: Maybe a -> Bool isJust Nothing = False isJust _ = True main :: IO () main = print $ isJust ( Just 0) { - Wywo ł uje b ł ą d : renamer . hs :8:16: A m b i g u o u s o c c u r r e n c e ‘ isJust ’ It could refer to either ‘ Main . isJust ’ , defined at renamer . hs :4:1 or ‘ Data . Maybe . isJust ’ , i m p o r t e d from ‘ Data . Maybe ’ at renamer . hs :1:20 -25 -} Fragment 2.4: Przykład działania renamera - shadowing. foo = 10 main :: IO () main = let foo = 20 in print foo -- Po u r u c h o m i e n i u wypisze 20 wywoływany jest błąd, jak w przykładzie 2.3. W algorytmie brane jest przy tym pod uwagę, czy nazwa jest kwalifikowana i w jaki sposób importowany jest inny moduł, co czasami pozwala usunąć wieloznaczność. Możliwe jest również zjawisko zwane shadowingiem, polegające na zastąpieniu umieszczonej w zakresie definicji przez definicję lokalną. Zachodzi ono na fragmencie 2.4. Wreszcie, jeżeli w danym miejscu w zakresie nie będzie nic o danej nazwie, to renamer zakończy działanie z błędem, jak w przykładzie 2.5. Na tym etapie kompilacji wykonywane jest również wiele pobocznych zadań, na przykład wyświetlenie ostrzeżeń, gdy pewna zmienna zostanie zdefiniowana, lecz nie będzie później użyta w wyrażeniu, gdy kompilacja została rozpoczęta z flagą -fwarn-unused-matches. To zachowanie jest jednym z usprawnień poruszonych w tej pracy. Fragment 2.5: Przykład działania renamera - nieodnalezienie w zakresie. main :: IO () main = print bar -- Wywo ł uje b ł ą d : -- r e n a m e r 3 . hs :2:14: Not in scope : ‘ bar ’ 9 ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW Fragment 2.6: Przykład bezsensownego wyrażenia w programie, z błędem GHC, który wywołuje. module Nonsense where nonsense = 10 + " a " { - B ł ą d k o m p i l a c j i w GHC : [1 of 1] C o m p i l i n g N o n s e n s e ( n o n s e n s e . hs , n o n s e n s e . o ) n o n s e n s e . hs :3:15: No i n s ta n c e for ( Num [ Char ]) arising from a use of ‘ + ’ In the e x p r e s s i o n : 10 + " a " In an e q u a t i o n for ‘ n o n s e n s e ’ : n o n s e n s e = 10 + " a " -} 2.1.3 Type checker Warto w tym miejscu wspomnieć o podstawowych pojęciach związanych z systemami typów. Częścią definicji języka jest jego gramatyka, która określa, jakie ciągi znaków można uznać za program w tym języku. W skład gramatyki wchodzą wyrażenia, które w wielu językach można podzielić na termy i typy. Termy pozwalają wyrażać obliczenia, dzięki zdefiniowanej dla nich semantyce. Jednym z rodzajów semantyki jest semantyka operacyjna, w której wykonywanie obliczeń modeluje się za pomocą maszyny stanowej. Term jest lub, w bardziej złożonych semantykach, zawiera się w stanie maszyny. Ponadto istnieje relacja przejścia, określającej stany do jakich przechodzi maszyna w wyniku obliczeń. Semantykę operacyjną określa się przez przez podanie reguł ewaluacji, z których wynika ta relacja. Wykonywanie programu może zatrzymać się w stanie uważanym za końcowy. Term w postaci, do której został znormalizowany w stanie końcowym, nazywamy wartością. Obliczenia mogą również nigdy nie zatrzymać lub utknąć w stanie, który nie jest końcowy, lecz dla którego relacja przejścia nie określa dalszych kroków. Termy, dla których ewaluacja daje taki rezultat, nazywamy niepoprawnymi lub bezsensownymi[21]. Przykład 2.6 pokazuje niepoprawny term. Typy są przyporządkowywane termom w relacji typowania, wynikającej z pewnych reguł typowania. Zbiór tych reguł składa się na system typów. System typów służy do wykrywania bezsensownych termów bez ich ewaluowania, często już na etapie kompilacji. System typów jest poprawny (sound) lub bezpieczny, jeżeli każdy term, który ma w nim przyporządkowany typ, jest sensowny. Z kolei jest zupełny (complete), jeżeli każdy sensowny term jest w nim otypowany. Systemy typów w popularnych językach są przeważnie poprawne, lecz niezupełne. Rozszerzenia systemu typów dążą do zmniejszenia liczby poprawnych programów, które są w systemie typów niedopuszczalne, co można określić jako zwiększenie jego ekspresywności. Rozszerzenia mogą doprowadzić do tego, że problem sprawdzenia, czy dany program jest poprawnie otypowany, stanie się nierozstrzygalny. Nie będzie wtedy możliwe stworzenie odpowiedniego algorytmu i zagwarantowanie, że proces kompilacji się zakończy. Może to być niedopuszczalne ze względu na oczekiwania 10 ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW Fragment 2.7: Przykład programu, dla którego statyczne sprawdzanie typów w GHC się nie zakończy. { - # L A N G U A G E TypeFamilies , U n d e c i d a b l e I n s t a n c e s # -} module Diverge where type family Diverge a where Diverge a = Diverge a evil :: Diverge Int evil = 0 Fragment 2.8: Przykład użycia polimorfizmu parametrycznego w Haskellu. { - # L A N G U A G E E x p l i c i t F o r A l l # -} twice :: ( a -> a ) -> a -> a twice f = f . f twice ’ :: forall a . ( a -> a ) -> a -> a twice ’ f = f . f main :: IO () main = do print $ twice not True print $ twice ( ’ - ’:) " Hello " -- True -- " - - Hello " użytkowników - programistów[21]. Polimorfizm to możliwość posiadania przez term więcej niż jednego przypisanego mu typu. Może on wtedy występować w miejscach, gdzie oczekiwany jest term jednego z tych typów. W polimorfizmie parametrycznym uzyskuje się to przez wprowadzenie do typu zmiennych, za które mogą być podstawiane inne typy. Termy otypowane w ten sposób muszą mieć sens niezależnie od tego, jakie typy zostaną przyporządkowane zmiennym, choć np. w Haskellu istnieje możliwość wprowadzenia ograniczeń na zmienne. Polimorfizm parametryczny odpowiada kwantyfikacji ogólnej z logiki. Typy takie mogą mieć składnię pozwalającą na zapisanie jawnego kwantyfikatora, wiążącego zmienne w typie, zamiast pozostawiania tych zmiennych jako niezwiązanych. W przykładzie 2.8 funkcja twice jest polimorficzna, w jednym miejscu zmienna a służy za Bool, a w drugim za String. Funkcja twice’ pokazuje wspomnianą jawną kwantyfikację, lecz poza tym jest równoważna poprzedniej funkcji. W języku Java polimorfizm parametryczny dostępny jest pod postacią typów generycznych. W polimorfizmie ad hoc term może mieć przypisane różne typy dzięki temu, iż ma wiele definicji. W zależności od typu wykorzystywana jest inna z nich. W Haskellu ten polimorfizm jest dostępny w postaci klas typów[21]. W przykładzie 2.9 funkcja foo występuje raz jako funkcja typu Bool -> Int i raz jako String -> Int. W każdym przypadku ma inną definicję, sensowną dla wybranego wtedy typu. W Javie polimorfizm ten występuje pod postacią przeładowania metod, jak w przykładzie 2.10 z metodą bar. 11 ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW Fragment 2.9: Przykład użycia polimorfizmu ad hoc w Haskellu. class C a where foo :: a -> Int i n s t a n c e C Bool where foo True = 1 foo _ = 0 i n s t a n c e C [ a ] where foo = length main :: IO () main = do print $ foo True print $ foo " Hello " -- 1 -- 5 Fragment 2.10: Przykład użycia polimorfizmu ad hoc w Javie. public class Parametric { public static void main ( String ... args ) { bar (10) ; // bar int 10 bar ( " str " ) ; // bar String str } static void bar ( int arg ) { System . out . println ( " bar int " + arg ) ; } static void bar ( String arg ) { System . out . println ( " bar String " + arg ) ; } } 12 ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW W kompilatorze sprawdzenie, czy program jest poprawnie otypowany, zachodzi w części zwanej type checkerem. Jednym z zadań type checkera jest wyświetlenie w razie błędu takich komunikatów, by pozwolić użytkownikowi zrozumieć problem i go naprawić. Z tego powodu aż do tego etapu kompilacji zachowywane są takie informacje jak lokalizacja w pliku wejściowym i identyfikatory wybrane przez użytkownika. 2.2 Rozszerzenia Glasgow Haskell Compiler Przez rozszerzenia GHC rozumiane są zaimplementowane w nim funkcje, które nie są częścią specyfikacji języka przedstawioną w Haskell 2010 Language Report. Zliczenie linii w rezultacie wywołania ghc --supported-extensions pozwala stwierdzić, że jest ich obecnie około stu. Rozszerzenia są opcjonalne i domyślnie wyłączone. Uaktywnia się je dyrektywą języka umieszczoną na początku pliku modułu lub przez podanie odpowiednich flag wśród argumentów wywołania kompilatora. Wszystkie zmiany wykonane w tej pracy wiążą się z pewnymi rozszerzeniami, dlatego warto je omówić. 2.2.1 Rodziny typów Rozszerzenie TypeFamilies Rodziny typów to funkcje operujące na typach w trakcie kompilacji. Stworzenie rodziny typów wymaga podania deklaracji rodziny z liczbą i rodzajami parametrów oraz pewnej liczby definicji lub równań. W przypadku rodzin związanych z klasą, deklaracja znajduje się w deklaracji klasy i musi wykorzystywać parametry klasy. Przykładem takiej rodziny jest Box z klasy T na fragmencie 2.11. Może dodatkowo posiadać równanie domyślne, o takim samym znaczeniu co domyślne definicje funkcji w klasie. Równania podaje się przy definiowaniu instancji klasy. W przypadku rodzin niezależnych od klasy, jak Cont z przykładu 2.11, deklarację tworzy się z użyciem słowa kluczowego family, a równania tworzy się ze słowem kluczowym instance. Rodziny typów dzielą się na rodziny synonimów typów, deklarowane ze słowem kluczowym type i rodziny typów danych, deklarowane ze słowem kluczowym data. Równania należące do rodzin synonimów tworzą synonimy typów, podobne do tych z nierozszerzonego Haskella. W przykładzie 2.12 Equal Int Int jest synonimem Int i oba równie dobrze mogą znaleźć się w deklaracji typu dla foo. Analogicznie, rodziny typów danych grupują typy danych data lub newtype. Na przykład Cont Int z przykładu 2.11 to nazwa typu danych, którego wartości konstruuje się z użyciem ContI0 lub ContI1. Rodziny synonimów mogą mieć postać otwartą i zamkniętą. W formie zamkniętej, wszystkie równania rodziny wymienione są w miejscu jej deklaracji. Mają tę zaletę wobec rodzin otwartych, iż poszczególne równania mogą na siebie nachodzić i w takim wypadku stosowany jest synonim, którego równanie jest pierwsze od 13 ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW Fragment 2.11: Przykład pozwiązanej z klasą i niezależnej rodziny typów. { - # L A N G U A G E T y p e F a m i l i e s # -} module TypeFamilies where class T a where data Box a store :: Box a -> a -> Box a i n s t a n c e T Int where data Box Int = BoxI0 | BoxI1 Int store BoxI0 n = BoxI1 n store ( BoxI1 acc ) n = BoxI1 $ acc + n i n s t a n c e T Bool where data Box Bool = BoxB0 Int Int store ( BoxB0 ts fs ) True = BoxB0 ( ts + 1) fs store ( BoxB0 ts fs ) _ = BoxB0 ts ( fs + 1) data family Cont a data i n s t a nc e Cont Int = ContI0 | ContI1 Int data i n s t a nc e Cont Bool = ContB0 Int Int class S a where put :: Cont a -> a -> Cont a i n s t a n c e S Int where put ContI0 n = ContI1 n put ( ContI1 acc ) n = ContI1 $ acc + n i n s t a n c e S Bool where put ( ContB0 ts fs ) True = ContB0 ( ts + 1) fs put ( ContB0 ts fs ) _ = ContB0 ts ( fs + 1) 14 ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW Fragment 2.12: Przykład otwartej i zamkniętej funkcji na typach z nachodzącymi na siebie dziedzinami. { - # L A N G U A G E T y p e F a m i l i e s # -} module TypeFamilies where type family Equal a b where Equal a a = a Equal a b = (a , b ) foo :: Equal Int Int foo = 10 type family Open a b type i n s t a n c e Open a a = a type i n s t a n c e Open a b = (a , b ) {families - o v e r l a p p i n g . hs :13:15: C o n f l i c t i n g family i n s t a n c e d e c l a r a t i o n s : Open a a -- Defined at families - o v e r l a p p i n g . hs :13:15 Open a b -- Defined at families - o v e r l a p p i n g . hs :14:15 -} góry. Przypomina to mechanizm dopasowania wzorców dostępny na poziomie termów. W przypadku rodzin otwartych, kolejność równań nie jest określona i mogą one być definiowane w wielu plikach, więc nachodzenie na siebie dziedzin równań powoduje błąd. Różnicę tę widać w przykładzie 2.12[24]. 2.2.2 Częściowe sygnatury typów Rozszerzenie PartialTypeSignatures W Haskellu, podczas definiowania wyrażenia, możliwe, i zgodne z dobrymi praktykami, jest podanie sygnatury typu. Jest to jednak opcjonalne, gdyż przy braku sygnatury w czasie sprawdzania typów dojdzie do próby rekonstrukcji. Istnieje jednak również inna, pośrednia opcja: podanie niepełnej sygnatury, zawierającej tak zwane symbole wieloznaczne, zapisywane w postaci podkreślnika. Domyślnie obecność wieloznacznika wywoła błąd, w którego treści znajdzie się zrekonstruowany typ, którym można uzupełnić sygnaturę, a kompilacja zostanie przerwana. Rozszerzenie PartialTypeSignatures zmieni jednak to zachowanie, sprawiając, iż zamiast błędu wygenerowane zostaną jedynie ostrzeżenia i obecność wieloznacznika nie będzie przyczyną zakończenia działania kompilatora. Dodatkowo ostrzeżenia te można ukryć uruchamiając GHC z flagą -fno-warn-partialtype-signatures. Przykład 2.13 demonstruje przebieg kompilacji z tym rozszerzeniem. Symbol wieloznaczny może także znaleźć się wśród ograniczeń klasowych jak w przykładzie 2.14. Wtedy w błędzie lub ostrzeżeniu, generowanym na takich samych zasadach, co w poprzednim przypadku, znajdą się wywnioskowane przez kompilator zero lub więcej ograniczeń. Oprócz wieloznacznika w sygnaturze mogą znaleźć się jawnie podane ograniczenia. 15 ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW Fragment 2.13: Przykład użycia anonimowego symbolu wieloznacznego w sygna turze typu. foo :: _ -> Bool foo = not { - Wywo ł a b ł ą d lub ostrze ż enie : partial . hs :3:8: Found hole ‘ _ ’ with type : Bool To use the i n f e r r e d type , enable P a r t i a l T y p e S i g n a t u r e s In the type s i g n a t u r e for ‘ foo ’ : _ -> Bool -} Fragment 2.14: Przykład użycia anonimowego symbolu wieloznacznego w ograniczeniach typu. bar :: _ = > [ a ] -> Bool bar ( x : y : _ ) = x == y bar _ = False { - Wywo ł a b ł ą d lub ostrze ż enie : partial - constr . hs :3:8: Found hole ‘ _ ’ with i n f e r r e d c o n s t r a i n t s : ( Eq a ) To use the i n f e r r e d type , enable P a r t i a l T y p e S i g n a t u r e s In the type s i g n a t u r e for ‘ bar ’ : _ = > [ a ] -> Bool -} Drugim rozszerzeniem związanym z częściowymi sygnaturami jest NamedWildCards. Sprawia ono, iż identyfikatory zaczynające się od podkreślnika są również traktowane jako wieloznaczniki i są traktowane w podobny sposób do tego zaprezentowanego w 2.13 i 2.14. Bez niego wyłącznie same podkreślniki są traktowane jako wieloznaczniki, a identyfikatory zaczynające się od podkreślników jako zwyczajne zmienne typów. Te opcjonalne aktywowane wieloznaczniki określane są jako wieloznaczniki nazwane, a te opisane poprzednio jako wieloznaczniki anonimowe. Warte uwagi jest, iż nazwane wieloznaczniki to nie zmienne typów. Pokazuje to przykład 2.15, gdzie po aktywowaniu rozszerzenia NamedWildCards podana sygnatura jest poprawna, z wywnioskowanym typem Bool w miejscu _x. Bez tego rozszerzenia z kolei jest ona niepoprawna, gdyż _x jest wtedy traktowana jako zwykła zmienna, a zatem foo jest wtedy funkcją polimorficzną, która powinna mieć sens dla każdego _x, co wyraźnie nie jest prawdą dla wyrażenia not _x[18]. 16 ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW Fragment 2.15: Demonstracja różnicy między zmienną z podkreślnikiem i nazwanym wieloznacznikiem. omodule Partial where foo :: _x -> ( Bool , _x ) foo a = ( True , not a ) foo :: _ -> Bool foo = not { - Wywo ł a b ł ą d lub ostrze ż enie : partial - named . hs :4:16: Couldn ’ t match e x p e c t e d type ‘ _x ’ with actual type ‘ Bool ’ ‘ _x ’ is a rigid type v a r i a b l e bound by the type s i g n a t u r e for foo :: _x -> ( Bool , _x ) at partial - named . hs :3:8 R e l e v a n t b i n d i n g s include a :: _x ( bound at partial - named . hs :4:5) foo :: _x -> ( Bool , _x ) ( bound at partial - named . hs :4:1) In the e x p r e s s i o n : not a In the e x p r e s s i o n : ( True , not a ) -} {- Z rozszerzeniem NamedWildCards : partial - named . hs :3:8: Found hole ‘ _x ’ with type : Bool To use the i n f e r r e d type , enable P a r t i a l T y p e S i g n a t u r e s In the type s i g n a t u r e for ‘ foo ’ : _x -> ( Bool , _x ) -} 17 ROZDZIAŁ 2. BUDOWA KOMPILATORÓW I SYSTEMY TYPÓW 18 Rozdział 3 Użyte narzędzia i technologie 3.1 Język programowania Glasgow Haskell Compiler został napisany w języku Haskell. Wykorzystano została w projekcie technika bootstraping, polegająca na tym, iż kompilator rozwijany jest w języku, który kompiluje i jedną z zależności potrzebną do zbudowania GHC jest on sam. Środowiskiem wykorzystanym do edycji kodu było IntelliJ IDEA z wtyczką zapewniającą wsparcie dla języka Haskell. Wtyczka ta niestety w pełni działa tylko z projektami systemu Cabal i nie jest dostosowana do prac nad GHC. Z tego powodu środowisko nie zapewniało na przykład ciągłego raportowania błędów w kodzie lub wyszukiwania deklaracji określonego typu, choć z tych udogodnień można korzystać w innych projektach. Testowanie i walidacja kompilatora są realizowane przez skrypty w języku Python. Jest on wymagany do zbudowania GHC również z tego powodu, że do stworzenia dokumentacji wykorzystane zostało narzędzie Sphinx, wymagające interpretera. W projekcie użyty został również język C do napisania środowiska uruchomieniowego, linkowanego z każdym programem skompilowanym przez GHC. W pozostałych częściach GHC można znaleźć dyrektywy preprocesora C, który GHC może wykorzystać zanim przejdzie do kompilacji. W tej pracy jednak kontakt z nim był znikomy. 3.2 3.2.1 Narzędzia Git Kod GHC znajduje się w repozytorium Git i jest publicznie dostępny do pobrania pod adresem http://git.haskell.org/ghc.git. Biblioteki wykorzystywane w kompilatorze również są przechowywane w repozytoriach Gita, lecz są osobnymi projektami, dołączonymi do GHC jako submoduły. Klon oficjalnego repozytorium znajduje się również w popularnym serwisie GitHub. Prawa zapisu do repozytorium są ograniczone[3]. 19 ROZDZIAŁ 3. UŻYTE NARZĘDZIA I TECHNOLOGIE Krokiem wymaganym do rozpoczęcia pracy było sklonowanie repozytorium i lokalne zbudowanie kompilatora ze źródeł. Po lokalnej edycji plików zmiany musiały zostać zapisane w repozytorium i opatrzone komentarzem stosującym się do konwencji opisanej na wiki GHC, zanim mogły zostać wysłane z użyciem systemu Phabricator. 3.2.2 Trac Trac to system do organizacji pracy programistów. Przechowuje listę zgłoszeń, czyli spójnych propozycji zmian w projekcie. Zapisywane są w nim zadania do wykonania oraz błędy wykryte w GHC, także przez zwykłych użytkowników. Chroni to je przed zapomnieniem w razie, gdyby nie było możliwe zajęcie się nimi od razu. Znajdują się w tym systemie również propozycje dodania nowych funkcji. Do każdego zgłoszenia może być dołączony opis, w przypadku błędu sposób reprodukcji, zapewnione jest miejsce na dyskusję i mechanizm śledzenia stanu wykonania zadania. Aby ułatwić organizację pracy, możliwe jest przypisanie do zgłoszeń właściciela, by zapobiec sytuacji, w której tę samą rzecz wykonuje wiele osób. Możliwe jest określenie priorytetu i planowanej wersji, w której zadanie powinno już być wykonane i późniejsze sprawdzanie, czy ten cel został osiągnięty. Ułatwione jest również umieszczanie odnośników do innych, powiązanych zgłoszeń czy do systemu Phabricator. Wszystkie te funkcje wykorzystywane są w GHC. Rysunek 3.1: Opis zgłoszenia w systemie Trac Przykład zgłoszenia widoczny jest na rysunku 3.1. Dotyczy ono jednego z wykonanych w pracy usprawnień, pozostałe również mają swoje opisy w systemie. Aktualizacja informacji na stronie Trac jest wymagana przez procedurę wprowadzania zmian w GHC[26]. 20 ROZDZIAŁ 3. UŻYTE NARZĘDZIA I TECHNOLOGIE 3.2.3 Phabricator [20] Phabricator wykorzystywany jest do inspekcji kodu w czasie zgłaszania ulepszenia oraz do automatycznej walidacji z wykorzystaniem narzędzia Harbormaster. Inspekcja jest ułatwiona, gdyż program prezentuje obok siebie wersję sprzed i po dokonaniu zmian, z wyróżnieniem zmodyfikowanych linii kodu. Daje też możliwość komentowania wybranych fragmentów. Rysunek 3.2: Informacje o proponowanej zmianie w kodzie na stronie Phabricator Na rysunku 3.2 widać opis jednego z ulepszeń dokonanego w tej pracy. Zgodnie z procedurą [26], aby zmiany zapisane w lokalnym repozytorium Git znalazły się na serwerze, łatka musi przejść wpierw przez proces rewizji w systemie Phabricator. Polega on na tym, iż inni programiści, szczególnie ci, którzy posiadają uprawnienia do zapisu do repozytorium, zapoznają się z przesłanym kodem. Zgłaszają oni swoje uwagi i decydują, czy zmiana może zostać naniesiona na repozytorium na serwerze, czy wymaga poprawek. Z chwilą wysłania proponowanych zmian w kodzie do Phabricatora, wykonane zostaje również automatyczne zbudowanie i przetestowanie nowej wersji GHC przez moduł Harbormaster na przeznaczonym do tego serwerze. Jeżeli walidacja wykryje błędy, jest to znak, iż konieczne są poprawki i oszczędza to czas poświęcony na inspekcję kodu przez programistów. Na rysunku 3.3 widać raport z Harbormastera do jednego z wprowadzonych w tej pracy usprawnień, w którym nie wykryto problemów. 21 ROZDZIAŁ 3. UŻYTE NARZĘDZIA I TECHNOLOGIE Rysunek 3.3: Wynik walidacji programu Harbormaster widoczny na stronie Phabricator Jak wspomniane zostało w sekcji 3.1, GHC jest budowany z użyciem innej wersji GHC. Aby zapewnić stabilność, istnieje wymaganie, że nowa wersja kompilatora musi być możliwa do skompilowania z wykorzystaniem dowolnej z dwóch poprzednich stabilnych wersji[26]. Sam proces budowania przebiega wieloetapowo. Najpierw, z użyciem starszej wersji kompilatora, budowane są biblioteki, zależności GHC. Następnie kompilowany w ten sposób jest sam GHC, czego rezultatem jest kompilator w nowszej wersji. Następnie te dwa kroki, budowania bibliotek i kompilacji GHC, są powtarzane, lecz z wykorzystaniem nowego kompilatora i to on jest uznawany za wynik całego procesu[5]. Dzięki temu sposobowi działania, usprawnienia w kompilacji są od razu zastosowane do kompilatora, przy jednym etapie wykorzystywane byłyby optymalizacje i sposób generowania kodu ze starszej wersji. Stanowi to również dobry test nowej wersji, gdyż jeżeli pojawiły się w niej nowe błędy, to jest duża szansa, iż ujawnią się one w czasie budowania kompilatora na drugim etapie. Uzyskany kompilator jest poddawany kilku tysiącom testów. W większości dzielą się one na trzy kategorie: testów, w których kompilacja pewnego programu musi zakończyć się powodzeniem; testów, których kompilacja musi zakończyć się błędem; testów, w których kompilacja musi się udać, a następnie sprawdzane jest zachowanie programu po uruchomieniu. Wiele testów polega na sprawdzeniu komunikatów kompilatora i porównaniu ze wzorcem. 22 Rozdział 4 Usprawnienia Glasgow Haskell Compiler 4.1 Zgłoszenie nr 10839 Consistent pretty-printing of type families Pierwszym z wykonanych w pracy usprawnień było ujednolicenie wyświetlania rodzin typów. Problem polegał na tym, iż w zależności od miejsca i rodzaju błędu, wyświetlane były one w różny sposób. Co gorsza, różniły się one nie tylko formatem, ale też tym, które informacje zawierają. Widać to na poniższych wycinkach z komunikatów generowanych przez kompilator. We fragmencie 4.2, z błędem w otwartej rodzinie typów, widoczna jest lewa strona równań i wskazania, w którym miejscu w pliku się one znajdują. We fragmencie 4.4 z zamkniętą rodziną typów widoczna jest zarówno lewa jak i prawa strona równania. Nie jest jednak podane miejsce wystąpienia. W 4.6 wyświetlany jest kwantyfikator wiążący zmienne równania i obie strony równania, ale brak jest wcięcia, przez co równanie jest wyrównanie w pionie z resztą komunikatu i cierpi czytelność komunikatu. Brak jest również lokalizacji. 4.1.1 Rozwiązanie Po zapoznaniu się z kodem GHC, udało się odnaleźć miejsca odpowiadające za każdy z tych komunikatów. Była to funkcja inaccessibleCoAxBranch z modułu TcValidity w przypadku 4.4 oraz funkcje pprCoAxBranch i pprCoAxBranchHdr z modułu Coercion w przypadkach odpowiednio 4.6 i 4.2. Znaleziono jeszcze inFragment 4.1: Fragment testu T7524 z dwoma równaniami otwartej rodziny typów będącymi w konflikcie. 4 5 6 type family F ( a :: k1 ) ( b :: k2 ) type i n s t a n c e F a a = Int type i n s t a n c e F a b = Bool 23 ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER Fragment 4.2: Błąd generowany przez kompilator w przypadku 4.1 przed wprowadzeniem zmian. T7524 . hs :5:15: Conflicting family instance declarations : F a a -- Defined at T7524 . hs :5:15 F a b -- Defined at T7524 . hs :6:15 Fragment 4.3: Fragment testu T6018 z zamkniętą rodziną typów z równaniami o nachodzących na siebie dziedzinach. 70 71 72 73 74 75 -- This is similar , except that the last e q u a t i o n c o n t a i n s c o n c r e t e type . -- it is o v e r l a p p e d it should be dropped with a warning type family Foo a = r | r -> a where Foo Int = Bool Foo Bool = Int Foo Bool = Bool Since Fragment 4.4: Ostrzeżenie generowane przez kompilator w przypadku 4.3 przed wprowadzeniem zmian. T6018 . hs :75:5: Warning : Type family instance equation is overlapped : Foo Bool = Bool Fragment 4.5: Fragment testu T6018fail z równaniem rodziny typów nie spełnia jącej warunku różnowartościowości. 64 65 66 -- This should fail because there is no way to d e t e r m i n e k from the RHS type family Fc ( a :: k ) ( b :: k ) = r | r -> k type i n s t a n c e Fc a b = Int Fragment 4.6: Błąd generowany przez kompilator w przypadku 4.5 przed wprowadzeniem zmian. T6018fail . hs :66:15: error : Type family equation violates injectivity annotation . Kind variable ‘ k ’ cannot be inferred from the right - hand side . ( enabling - fprint - explicit - kinds might help ) In the type family equation : forall ( k :: BOX ) ( a :: k ) ( b :: k ) . Fc a b = Int 24 ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER Fragment 4.7: Fragment testu T9371 z rodziną typów danych powiązaną z klasą C i równaniami o nachodzących na siebie dziedzinach. 13 14 15 16 17 18 19 i n s t a n c e { - # O V E R L A P P A B L E # -} Monoid x = > C x where data D x = D1 ( Either x () ) makeD = D1 ( Left mempty ) i n s t a n c e ( Monoid x , Monoid y ) = > C (x , y ) where data D (x , y ) = D2 (x , y ) makeD = D2 ( mempty , mempty ) ną funkcję, pprCoAxiom, używaną do generowania komunikatów przy kompilacji z flagą -ddump-types. Wszystkie one operowały na typie danych CoAxBranch, odpowiadającym równaniu rodziny typów oraz CoAxiom, który odpowiada deklaracji rodziny typów i odrobinę różni się w zależności od tego, czy rodzina jest otwarta czy zamknięta. Główną różnicą jest to, czy posiada więcej niż jedną CoAxBranch. Mimo tego podobieństwa, które uczyniło ich ujednolicenie w tej pracy prostym zadaniem, zostały one zaimplementowane osobno. Funkcja inaccessibleCoAxBranch generuje ostrzeżenie w sytuacji, gdy równanie w zamkniętej rodzinie typów nigdy nie może zostać zastosowane z powodu ogólniejszego równania wymienionego wcześniej. pprCoAxBranchHdr używana jest do zgłaszania błędów w otwartych rodzinach typów, gdy dziedziny dwóch równań nachodzą na siebie. Z kolei pprCoAxBranch wykorzystywany jest do generowania błędów związanych z naruszeniem zadeklarowanej różnowartościowości rodziny typów. Różne warunki wystąpienia sprawiły, że funkcje znalazły się w różnych miejscach programu, w kilku modułach i w komunikacie skupiały się na różnych informacjach. Stąd nietrudno było o rozbieżności. Dokonano usprawnienia, które polegało na zrefaktorowaniu kodu i wydzieleniu części wspólnej do osobnej funkcji ppr_co_ax_branch. Została ona napisana tak, by wyświetlać wszystkie informacje, które wydobywały zastąpione funkcje. W komunikacie znajduje się zatem jawny kwantyfikator, wyświetlany tylko przy kompilacji z odpowiednią flagą lub, jeżeli rodzina typów wykorzystuje polimorfizm na poziomie rodzajów jak w przykładzie 4.6, obie strony równania i miejsce wystąpienia w kodzie kompilowanego programu. W trakcie pracy okazało się, że rodziny typów danych wymagają innego sposobu wyświetlania prawej strony równania. Jest tak, gdyż ich reprezentacja na etapie sprawdzania typów zawiera wygenerowany przez kompilator konstruktor typu, o którym użytkownik może nie wiedzieć. Z tego powodu pprCoAxBranch używa funkcji pprDataCons, która wydobywa z tego wewnętrznego konstruktora typu konstruktory wartości i odtwarza prawą stronę równania taką, jaką podał użytkownik. Przykład działania widoczny jest we fragmencie 4.9. Wcześniej prawa strona nie była w tym przypadku wcale wyświetlana, co widać na wycinku 4.8. Inaczej wygląda to w przypadku funkcji pprCoAxiom, która jest używana przy generowaniu zrzutu z type checkera przy kompilacji z odpowiednią flagą. Tutaj pożądane jest wyświetlenie wewnętrznej reprezentacji i pprDataCons nie jest używana. Przykładowy efekt widać na przykładzie 4.10. 25 ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER Fragment 4.8: Błąd generowany przez kompilator w przypadku 4.8 przed wprowa dzeniem zmian. T9371 . hs :14:10: Conflicting family instance declarations : D -- Defined at T9371 . hs :14:10 D (x , y ) -- Defined at T9371 . hs :18:10 Fragment 4.9: Błąd generowany przez kompilator w przypadku 4.9 przed wprowadzeniem zmian. T9371 . hs :14:10: Conflicting family instance declarations : D = D1 ( Either x () ) -- Defined at T9371 . hs :14:10 D (x , y ) = D2 (x , y ) -- Defined at T9371 . hs :18:10 Przy okazji tego zadania wykonany został również drobny refaktoring, polegający na dodaniu funkcji vbar, reprezentującej komunikat "|". Definiowane wcześniej lokalnie wystąpienia tego komunikatu zostały zastąpione. 4.1.2 Testy Do tego zadania nie zostały utworzone nowe testy. Jako że wprowadzona zmiana dotyczyła komunikatów o błędach, istniejące już testy pokrywały ją w wystarczającym stopniu. Dwadzieścia dziewięć testów wykazało wprowadzone przy tym zgłoszeniu zmiany. Zostały one uaktualnione, by oczekiwać nowych komunikatów. Fragmenty testów 4.1, 4.5 i 4.3 zaprezentowane wcześniej również wywołały inne komunikaty, które przedstawiono na wycinkach 4.1, 4.5 i 4.3. Wszystkie trzy komunikaty mają ten sam format. Zawierają obie strony równania i lokalizację oraz takie same wcięcia, wyróżniające je od reszty błędu. Dwa fragmenty, w których użyta została zmienna k, są wyświetlane z kwantyfikatorem, choć wcześniej w przypadku 4.1 informacja o polimorfizmie na poziomie rodzajów typów była niewidoczna. Fragment 4.10: Fragment oczekiwanego zrzutu z type checkera testu DataFamily InstanceLHS z wyświetlonym równaniem rodziny typów danych. COERCION AXIOMS axiom D a t a F a m i l y I n s t a n c e L H S . TFCo : R : SingMyKind_ :: Sing = D a t a F a m i l y I n s t a n c e L H S . R : SingMyKind_ -- Defined at D a t a F a m i l y I n s t a n c e L H S . hs :8:15 26 ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER Fragment 4.11: Błąd generowany przez kompilator w przypadku 4.1 po wprowa dzeniu zmian. T7524 . hs :5:15: Conflicting forall ( k forall ( k F a b = family instance declarations : :: BOX ) ( a :: k ) . F a a = Int -- Defined at T7524 . hs :5:15 :: BOX ) ( k1 :: BOX ) ( a :: k ) ( b :: k1 ) . Bool -- Defined at T7524 . hs :6:15 Fragment 4.12: Ostrzeżenie generowane przez kompilator w przypadku 4.3 po wpro wadzeniu zmian. T6018 . hs :75:5: Warning : Type family instance equation is overlapped : Foo Bool = Bool -- Defined at T6018 . hs :75:5 4.2 Zgłoszenie nr 10982 Warn about unused pattern variables in type families Zgłoszenie to polegało na dodaniu ostrzeżeń o nieużywanych zmiennych w funkcjach na poziomie typu, działających analogicznie do obecnych już w GHC ostrzeżeń w funkcjach na poziomie termów. Są one uaktywniane tą samą flagą -fwarnunused-matches, domyślnie nie są wyświetlane. Ważnym zagadnieniem okazało się zdefiniowanie, kiedy zmienna typu uważana jest za używaną. W zaimplementowanej wersji, by ostrzeżenie nie było generowane, zmienna musi występować po prawej stronie równania lub występować przynajmniej dwa razy po lewej stronie równania. Wartym uwagi jest, iż ten drugi przypadek nie występuje w przypadku funkcji na poziomie termów. Jest tak, gdyż wielokrotne użycie tej samej zmiennej we wzorcach zwykłej funkcji jest uważane za próbę wprowadzenia konfliktowych definicji i zawsze generuje błąd. Z kolei w rodzinach typów jest to zupełnie dopuszczalne i oznacza, że takie równanie ma zastosowanie wyłącznie wtedy, gdy za zmienną podstawiony jest ten sam typ we wszystkich miejscach jej wystąpienia. Zatem definicja funkcji equal a a = True jest niepoprawna i w szczególności nie pozwala wyrazić tego, iż oba argumenty muszą mieć równe wartości. Natomiast równanie rodziny typów Equal a a = () jest poprawne i zostanie wykorzystane przy aplikacji Equal do dwóch równych ty- Fragment 4.13: Błąd generowany przez kompilator w przypadku 4.5 po wprowa dzeniu zmian. T6018fail . hs :66:15: error : Type family equation violates injectivity annotation . Kind variable ‘ k ’ cannot be inferred from the right - hand side . ( enabling - fprint - explicit - kinds might help ) In the type family equation : forall ( k :: BOX ) ( a :: k ) ( b :: k ) . Fc a b = Int -- Defined at T6018fail . hs :66:15 27 ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER Fragment 4.14: Przykład funkcji na poziomie typów ze zmiennymi uznawanymi za wykorzystywane. type family X a where X a = a type family Y a b where Y a a = Int Fragment 4.15: Przykład funkcji na poziomie typów ze zmiennymi zastąpionymi lub poprzedzonymi podkreślnikami. type family D a b where D a _b = a type family E a b where E a _ = a pów. Ze względu na to zachowanie, zmienne typów występujące wielokrotnie we wzorcach są uznawane za używane. We fragmencie 4.14 zmienna a jest uznawana za używaną w rodzinie X ze względu na pierwszy warunek, a w Y ze względu na drugi. Aby ostrzeżenie nie wystąpiło, gdy zmienna jest nieużywana, należy zastąpić ją lub poprzedzić podkreślnikiem. Pokazane to zostało we fragmencie 4.15. 4.2.1 Rozwiązanie Wyświetlania ostrzeżeń dokonano przez zmiany w renamerze. Po analizie kodu udało się stwierdzić, że funkcja rnFamInstDecl z modułu RnSource odpowiada za wszystkie rodziny typów w kodzie, otwarte, zamknięte i związane z klasą. Ponadto jest już w niej używana funkcja extractHsTysRdrTyVars z modułu RnTypes, która przechodzi po wzorcach po lewej stronie równania, kolekcjonując nazwy zmiennych, by utworzyć z nich zmienne typów. Zbiór zmiennych używanych to z kolei to samo, co zbiór zmiennych wolnych po prawej stronie równania, zwracany przez funkcję rnPayload. Większość informacji potrzebnych do realizacji założeń zgłoszenia było zatem dostępnych od początku, należało pozyskać informacje o duplikatach. W tym celu funkcja extractHsTysRdrTyVars zastąpiona została bardzo podobną extractHsTysRdrTyVarsDups, która zwraca wszystkie wystąpienia zmiennych z lewej strony równania rodziny typów, nie usuwając wielokrotnych wystąpień tej samej nazwy. To umożliwiło sprawdzanie drugiego z warunków, kiedy zmienna jest uznawana za używaną. Do wygenerowania ostrzeżeń użyta została funkcja warnUnusedMatches, ta sama, która odpowiada za ostrzeżenia w funkcjach na poziomie termów. Gwarantuje to, że komunikaty są w tym samym formacie. Następuje w niej również odczytanie ze środowiska globalnego w monadzie renamera, czy opcja generowania ostrzeżeń została wybrana przez programistę, na przykład przez ustawienie flagi -fwarnunused-matches lub -Wall. 28 ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER Fragment 4.16: Fragment testu UnusedTyVarWarnings z równaniami wywołującymi ostrzeżenia lub zawierającymi zmienne poprzedzone podkreślnikiem. 7 8 9 10 11 12 13 14 15 16 17 type family C a b where C a b = a -- should warn type family C2 a b type i n s t a n c e C2 a b = a -- should warn type family D a b where D a _b = a -- should not warn type family D2 a b type i n s t a n c e D2 a _b = a -- should not warn Zmienne o identyfikatorach zaczynających się od podkreślnika nie powinny wywoływać ostrzeżeń niezależnie od tego czy są używane. Ich ignorowanie zachodzi w funkcji warnUnusedMatches z RnEnv przez zwyczajne sprawdzenie jaki jest pierwszy znak w nazwie. Z kolei anonimowe i nazwane symbole wieloznaczne nie wywołują ostrzeżeń, gdyż nie są kolekcjonowane w funkcji extractHsTysRdrTyVarsDups, więc nigdy nie znajdują się na zwróconej przez nią liście nazw. Zmiana wymagała przejrzenia rodzin typów w GHC oraz powiązanych bibliotekach i uaktualnienia ich tak, by nie generowały ostrzeżeń. W przeciwnym razie proces walidacji kończyłby się błędem, gdyż został on skonfigurowany, by nie dopuszczać ostrzeżeń w pewnych częściach projektu. Z tego powodu konieczne było stworzenie niewielkiej łatki do biblioteki Hoopl, w której poprzedzono problematyczne zmienne podkreślnikami. Bez żadnych przeszkód znalazła się ona w repozytorium tego projektu. 4.2.2 Testy Testami sprawdzającymi działanie usprawnienia są UnusedTyVarWarnings i UnusedTyVarWarningsNamedWCs. Oba działają przez próbę kompilacji pewnego programu i sprawdzenie, czy na wyjściu kompilatora pojawiają się oczekiwane ostrzeżenia. Program ten zawiera jedenaście przykładów rodzin typów otwartych, zamkniętych lub rodzin typów danych. Wśród ich równań są przypadki zmiennych nieużywanych, używanych ze względu na wystąpienie po prawej stronie i używanych ze względu na wielokrotne wystąpienie po lewej stronie, z nazwą poprzedzoną podkreślnikiem oraz symboli wieloznacznych. Testy te różnią się od siebie tym, iż w przypadku UnusedTyVarWarningsNamedWCs kompilacja zachodzi z dodatkowo uaktywnionym rozszerzeniem NamedWildCards. Ma to związek ze zgłoszeniem opisanym w sekcji 4.3. Fragment 4.16 to część jednego z opisanych testów. Z kolei fragment 4.17 to ostrzeżenia, które są dla niego oczekiwane. Dodanie nowej funkcji wymagało jeszcze uaktualnienia dokumentacji. Informacje o dodanych ostrzeżeniach zostały dodane do przewodnika użytkownika w części z opisem opcjonalnych ostrzeżeń, w części z opisem rodzin typów i do dziennika zmian. 29 ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER Fragment 4.17: Ostrzeżenia generowany przez kompilator dla fragmentu 4.16 po wprowadzeniu zmian. U n u s e d T y V a r W a r n i n g s . hs :8:5: warning : Defined but not used : type variable ‘ b ’ U n u s e d T y V a r W a r n i n g s . hs :11:18: warning : Defined but not used : type variable ‘ b ’ Fragment 4.18: Fragment testu NamedWildcardExplicitForall sprawdzającego, czy zmienne związane kwantyfikatorem nie zostają zamienione w wieloznaczniki. 13 14 15 16 17 baz :: forall _a . _a -> _b -> ( _a , _b ) baz x y = ( not x , not y ) -- _a is a variable , _b is a w i l d c a r d qux :: _a -> ( forall _a . _a -> _a ) -> _a qux x f = let _ = f 7 in not x -- the _a bound by forall is a tyvar -- the other _a are w i l d c a r d s 4.3 Zgłoszenie nr 11098 PartialTypeSignatures mishandles type variables that begin with an underscore W tym zgłoszeniu konieczne było naprawienie błędu związanego z rozszerzeniem NamedWildCards. Problem ten napotkany został w czasie pracy nad zgłoszeniem nr 10982 opisanym w sekcji 4.2, gdy zbadano jak uaktywnienie tego rozszerzenia wpływa na nazwane symbole wieloznaczne w rodzinach typów. Dlatego został wybrany po tamtym zgłoszeniu. Opis błędu początkowo skupiał się na przypadku, w którym zmienna typu z nazwą zaczynającą się od podkreślnika, była traktowana jako nazwany symbol wieloznaczny, mimo tego, iż była jawnie związana kwantyfikatorem. Przypadek ten został pokazany na wycinku 4.18. Problem okazał się jednak wpływać na więcej programów. Uaktywnienie NamedWildCards powodowało błędy kompilacji, gdy zmienna typu zaczynająca się od podkreślnika znalazła się poza sygnaturą typu, na przykład w deklaracji synonimu typu, typu danych, klasy czy rodziny typów. Znaczenie symboli wieloznacznych w sygnaturach jest dobrze określone w specyfikacji PartialTypeSignatures, lecz nie są one oczekiwane poza nimi. Przykładowy kod demonstrujący błędy w różnych kontekstach znajduje się we fragmencie 4.19. Jak widać z uaktywnioną opcją NamedWildCards kompilacja wielu poprawnych programów kończyła się błędem. Zmienne zaczynające się od podkreślnika są w Haskellu od dawna i nie ma powodu, by w tej sytuacji były niedopuszczalne. Zgłoszenie polegało na dokonaniu takich zmian w GHC, by przestały one być źródłem błędów. 30 ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER Fragment 4.19: Deklaracje, w których kompilacja kończyła się błędem, przy ak tywnym rozszerzeniu NamedWildCards. type Synonym _a = _a -> _a -- " U n e x p e c t e d type ’_a ’ In the type d e c l a r a t i o n for ’ Synonym ’" data A a _b = ACon a a -- " U n e x p e c t e d type ’_b ’ In the data d e c l a r a t i o n for ’A ’" data B _a b = BCon _a _a -- " U n e x p e c t e d type ’_a ’ In the data d e c l a r a t i o n for ’B ’" type family C a b where C a _b = a -> a -- " W i l d c a r d ’_b ’ not allowed in a type pattern of family i n s t a n c e for ’C ’" type family D a b where D _a b = _a -> _a -- " W i l d c a r d ’_a ’ not allowed in a type pattern of family i n s t a n c e for ’D ’" -- " W i l d c a r d ’_a ’ not allowed in the d e c l a r a t i o n for type synonym ’D ’" ( twice ) data family E a b data i n s t a n c e E a _b = ECon a a -- " W i l d c a r d ’_b ’ not allowed in a type pattern of family i n s t a n c e for ’E ’" data family F a b data i n s t a n c e F _a b = FCon _a _a -- " W i l d c a r d ’_a ’ not allowed in a type pattern of family i n s t a n c e for ’F ’" -- " W i l d c a r d ’_a ’ not allowed in the d e f i n i t i o n of data c o n s t r u c t o r ’ FCon ’" ( twice ) 4.3.1 Rozwiązanie Źródłem problemów było rozwinięcie tyvar w definicji gramatyki programu Happy. Początkowo generowało ono taki parser, który w sytuacji, gdy rozszerzenie NamedWildCards jest aktywne, parsował wszystkie zmienne zaczynające się od podkreślników jako nazwane wieloznaczniki. Wykorzystany pomysł na rozwiązanie problemu był taki, by parsować wszystkie identyfikatory jako zwykłe zmienne typów, a następnie zamieniać część z nich w renamerze. Rzeczywiście, w ramach pracy dokonano zmian w Parser.y, by bezwarunkowo konstruowane były HsTyVar, reprezentujące zmienne typu. Do rekordu LocalRdrEnv przekazywanego w monadzie renamera RnM dodane zostało pole lre_nwcs, będące zbiorem nazw zmiennych, które powinny zostać zastąpione przez symbole wieloznaczne. Rozwiązanie problemu z fragmentu 4.19 uzyskano poprzez kontrolę, kiedy ten zbiór ma być wypełniany. Wolne zmienne, zaczynające się od podkreślnika, umieszczane są w nim bowiem wyłącznie w funkcjach, które operują na typie LHsSigWcType. Tym bowiem cechują się funkcje, którymi dokonywane jest zastępowanie nazw w kontekstach dopuszczających nazwane symbole wieloznaczne. Do tego zbiór pozostaje pusty, gdy opcja NamedWildCards jest nieaktywna. Zbiór ten jest modyfikowany w funkcjach rnWcSigTy i rnHsTyKi, w wariantach oczekujących argumentów o wartości HsForAllTy, czyli reprezentujących jawne kwantyfikatory. Podczas zastępowania nazw w ciele kwantyfikatora, nazwy zwią31 ROZDZIAŁ 4. USPRAWNIENIA GLASGOW HASKELL COMPILER zane nim są usunięte ze zbioru lre_nwcs. Nie są one zamieniane w wieloznaczniki, co rozwiązuje problem z fragmentu 4.18. lre_nwcs jest wykorzystywany w wariancie funkcji rnHsTyKi przyjmującym argument HsTyVar. Do zamiany zmiennej typu na wieloznacznik dochodzi, gdy nazwa zmiennej zostanie w nim odnaleziona. W przeciwnym razie argument traktowany jest jako zwykła zmienna. 4.3.2 Testy Poprawność rozwiązania weryfikują trzy testy. T11098 to skrypt w GHCi, który znalazł się w opisie błędu. NamedWildcardsAsTyVars sprawdza, czy uaktywnienie rozszerzenia NamedWildCards nie powoduje błędów kompilacji w poprawnych programach, lecz zawierających zmienną zaczynająca się od podkreślnika w kontekście innym niż sygnatura typu, podobnie jak w przykładzie 4.19. NamedWildcardExplicitForall testuje, czy zmienne związane przez jawnie podane kwantyfikatory są traktowane jak zwykłe zmienne, a nie zamieniane w symbole wieloznaczne. Przypomina on fragment 4.18. 32 Rozdział 5 Podsumowanie i wnioski 5.1 Dyskusja wyników Dzięki zrealizowaniu pracy, sposób wyświetlania rodzin typów w błędach i ostrzeżeniach kompilatora jest ujednolicony. Dodane zostało wyświetlanie prawych stron równań rodzin typów danych w sposób naśladujący składnię tych konstrukcji. Stary sposób wyświetlania, oddający wewnętrzną reprezentację rodzin typów danych, został zachowany w zrzucie z etapu sprawdzania typów. Dodane zostało także opcjonalne wyświetlanie ostrzeżeń o nieużywanych zmiennych w rodzinach typów. Zmienna uważana jest za nieużywaną jeżeli występuje we wzorcach po lewej stronie równania tylko raz i nie występuje w typie po prawej stronie. Poprawiony został błąd z zamianą zmiennych typów zaczynających się od podkreślnika w symbole wieloznaczne w kontekstach, gdzie są one niedozwolone. Algorytm renamera został również zmieniony tak, by nie dokonywał zamiany zmiennych jawnie związanych kwantyfikatorem. Dzięki temu uaktywnienie rozszerzenia NamedWildCards nie powoduje już odrzucania poprawnych programów. Wszystkie te trzy modyfikacje przeszły proces rewizji kodu w systemie Phabricator i znalazły się w repozytorium. Od tamtej pory wprowadzony kod podlegał dodatkowym modyfikacjom i refaktoryzacji dokonanej przez innych programistów. W szczególności część dotycząca NamedWildCards została w dużej części zastąpiona przez alternatywne rozwiązanie Simona Peytona Jonesa. Z kolei ostrzeżenia o nieużywanych zmiennych typów w wyniku zgłoszenia nr 11451 są uaktywniane nową flagą -Wunused-type-variables zamiast -Wunused-matches. Jednak wszystkie wprowadzone w tej pracy usprawnienia dalej są dostępne w kompilatorze, dlatego cele należy uznać za zrealizowane. 5.2 Perspektywy dalszych badań System Trac na stronie GHC zawiera obecnie ponad 1600 otwartych zgłoszeń[22]. W innych pracach można podjąć się dokonania następnych usprawnień związanych z programowaniem z użyciem typów lub z inną częścią kompilatora. Złożone pro33 ROZDZIAŁ 5. PODSUMOWANIE I WNIOSKI pozycje mają poświęcone sobie podstrony na wiki GHC, gdzie można znaleźć ich planowane funkcje, projekty, opisy przebiegu implementacji i odnośniki do prac badawczych, na których bazują. Są to na przykład propozycja dodania typów zależnych do Haskella lub wprowadzenia definiowanych przez użytkownika błędów typów. Praca mogłaby polegać również na sformułowaniu nowej propozycji i zaimplementowaniu jej. Możliwości są szerokie i wiele osób zdecydowało się już poświęcić swój czas GHC. 34 Bibliografia [1] Alfred Aho. Compilers : principles, techniques, and tools. Pearson, Harlow, Essex, 2014. [2] Alex Documentation. https://www.haskell.org/alex/doc/alex.pdf. (Stan na dzień: 2015-01-31). [3] Building/GettingTheSources - GHC. https://ghc.haskell.org/trac/ghc/ wiki/Building/GettingTheSources. (Stan na dzień: 2015-01-31). [4] Building/Preparation - GHC. https://ghc.haskell.org/trac/ghc/wiki/ Building/Preparation. (Stan na dzień: 2015-01-31). [5] Building/Using - GHC. https://ghc.haskell.org/trac/ghc/wiki/ Building/Using. (Stan na dzień: 2015-01-31). [6] Manuel M. T. Chakravarty, Gabriele Keller, Simon Peyton Jones. Associated type synonyms. Proceedings of the Tenth ACM SIGPLAN International Conference on Functional Programming, ICFP ’05, strony 241–253, New York, NY, USA, 2005. ACM. [7] Manuel M. T. Chakravarty, Gabriele Keller, Simon Peyton Jones, Simon Marlow. Associated types with class. Proceedings of the 32Nd ACM SIGPLANSIGACT Symposium on Principles of Programming Languages, POPL ’05, strony 1–13, New York, NY, USA, 2005. ACM. [8] James Cheney, Ralf Hinze. First-class phantom types. Raport instytutowy, Cornell University, 2003. [9] Richard A. Eisenberg, Dimitrios Vytiniotis, Simon Peyton Jones, Stephanie Weirich. Closed type families with overlapping equations. Proceedings of the 41st ACM SIGPLAN-SIGACT Symposium on Principles of Programming Languages, POPL ’14, strony 671–683, New York, NY, USA, 2014. ACM. [10] GHC/Type families - HaskellWiki. https://wiki.haskell.org/GHC/ Indexed_types. (Stan na dzień: 2015-01-31). [11] Happy Documentation. https://www.haskell.org/happy/doc/happy.pdf. (Stan na dzień: 2015-01-31). 35 BIBLIOGRAFIA [12] Implementations HaskellWiki. https://wiki.haskell.org/ Implementations. (Stan na dzień: 2015-01-31). [13] InjectiveTypeFamilies - GHC. https://ghc.haskell.org/trac/ghc/wiki/ InjectiveTypeFamilies. (Stan na dzień: 2015-01-31). [14] Mark P. Jones. Type classes with functional dependencies. Gert Smolka, redaktor, Programming Languages and Systems, wolumen 1782 serii Lecture Notes in Computer Science, strony 230–244. Springer Berlin Heidelberg, 2000. [15] Miran Lipovača. Learn you a Haskell for great good! a beginner’s guide. No Starch Press, San Francisco, Calif, 2011. [16] S. Marlow, S. Peyton Jones. The Glasgow Haskell Compiler. A. Brown, G. Wilson, redaktorzy, The Architecture of Open Source Applications, wolumen II, rozdzia/l 5. 2012. [17] Newcomers - GHC. https://ghc.haskell.org/trac/ghc/wiki/Newcomers. (Stan na dzień: 2015-01-31). [18] Partial Type Signatures. https://downloads.haskell.org/˜ghc/latest/ docs/html/users_guide/partial-type-signatures.html. (Stan na dzień: 2015-01-31). [19] Simon Peyton Jones, Dimitrios Vytiniotis, Stephanie Weirich, Geoffrey Washburn. Simple unification-based type inference for gadts. Proceedings of the Eleventh ACM SIGPLAN International Conference on Functional Programming, ICFP ’06, strony 50–61, New York, NY, USA, 2006. ACM. [20] Phabricator - GHC. https://ghc.haskell.org/trac/ghc/wiki/ Phabricator. (Stan na dzień: 2015-01-31). [21] Benjamin Pierce. Types and programming languages. MIT Press, Cambridge, Mass, 2002. [22] Status/Tickets - GHC. https://ghc.haskell.org/trac/ghc/wiki/ Status/Tickets. (Stan na dzień: 2015-01-31). [23] Bryan Sullivan. Real world Haskell. O’Reilly, Sebastopol, CA, 2009. [24] Type families. https://downloads.haskell.org/˜ghc/latest/docs/ html/users_guide/type-families.html. (Stan na dzień: 2015-01-31). [25] UHCUserDocumentation < Ehc < UCCS. http://foswiki.cs.uu.nl/ foswiki/Ehc/UhcUserDocumentation. (Stan na dzień: 2015-01-31). [26] WorkingConventions/FixingBugs - GHC. https://ghc.haskell.org/trac/ ghc/wiki/WorkingConventions/FixingBugs. (Stan na dzień: 2015-01-31). 36 BIBLIOGRAFIA [27] Brent A. Yorgey, Stephanie Weirich, Julien Cretin, Simon Peyton Jones, Dimitrios Vytiniotis, José Pedro Magalhães. Giving haskell a promotion. Proceedings of the 8th ACM SIGPLAN Workshop on Types in Language Design and Implementation, TLDI ’12, strony 53–66, New York, NY, USA, 2012. ACM. 37 BIBLIOGRAFIA 38 Spis rysunków 2.1 Schemat Glasgow Haskell Compiler . . . . . . . . . . . . . . . . . . 3.1 3.2 3.3 Opis zgłoszenia w systemie Trac . . . . . . . . . . . . . . . . . . . . 20 Informacje o proponowanej zmianie w kodzie na stronie Phabricator 21 Wynik walidacji programu Harbormaster widoczny na stronie Phabricator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 39 6 SPIS RYSUNKÓW 40 Spis fragmentów programów 2.1 2.2 2.3 2.4 2.5 2.6 2.7 2.8 2.9 2.10 2.11 2.12 2.13 2.14 2.15 4.1 4.2 4.3 4.4 4.5 4.6 Wycinki z pliku Lexer.x składające się na reguły opisujące co jest wyodrębniane jako zmienna i konstruktor. . . . . . . . . . . . . . . Wycinki z pliku Parser.y z produkcjami odpowiadającymi za zmienne typów, wykorzystujące tokeny, których dotyczył przykład 2.1. . . Przykład działania renamera - niejednoznaczne odwołanie. . . . . . Przykład działania renamera - shadowing. . . . . . . . . . . . . . . Przykład działania renamera - nieodnalezienie w zakresie. . . . . . . Przykład bezsensownego wyrażenia w programie, z błędem GHC, który wywołuje. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Przykład programu, dla którego statyczne sprawdzanie typów w GHC się nie zakończy. . . . . . . . . . . . . . . . . . . . . . . . . . Przykład użycia polimorfizmu parametrycznego w Haskellu. . . . . Przykład użycia polimorfizmu ad hoc w Haskellu. . . . . . . . . . . Przykład użycia polimorfizmu ad hoc w Javie. . . . . . . . . . . . . Przykład pozwiązanej z klasą i niezależnej rodziny typów. . . . . . Przykład otwartej i zamkniętej funkcji na typach z nachodzącymi na siebie dziedzinami. . . . . . . . . . . . . . . . . . . . . . . . . . . Przykład użycia anonimowego symbolu wieloznacznego w sygnaturze typu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Przykład użycia anonimowego symbolu wieloznacznego w ograniczeniach typu. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Demonstracja różnicy między zmienną z podkreślnikiem i nazwanym wieloznacznikiem. . . . . . . . . . . . . . . . . . . . . . . . . . Fragment testu T7524 z dwoma równaniami otwartej rodziny typów będącymi w konflikcie. . . . . . . . . . . . . . . . . . . . . . . . . . Błąd generowany przez kompilator w przypadku 4.1 przed wprowadzeniem zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragment testu T6018 z zamkniętą rodziną typów z równaniami o nachodzących na siebie dziedzinach. . . . . . . . . . . . . . . . . . . Ostrzeżenie generowane przez kompilator w przypadku 4.3 przed wprowadzeniem zmian. . . . . . . . . . . . . . . . . . . . . . . . . . Fragment testu T6018fail z równaniem rodziny typów nie spełniającej warunku różnowartościowości. . . . . . . . . . . . . . . . . . . Błąd generowany przez kompilator w przypadku 4.5 przed wprowadzeniem zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41 8 8 9 9 9 10 11 11 12 12 14 15 16 16 17 23 24 24 24 24 24 SPIS FRAGMENTÓW PROGRAMÓW 4.7 4.8 4.9 4.10 4.11 4.12 4.13 4.14 4.15 4.16 4.17 4.18 4.19 Fragment testu T9371 z rodziną typów danych powiązaną z klasą C i równaniami o nachodzących na siebie dziedzinach. . . . . . . . . . Błąd generowany przez kompilator w przypadku 4.8 przed wprowadzeniem zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Błąd generowany przez kompilator w przypadku 4.9 przed wprowadzeniem zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Fragment oczekiwanego zrzutu z type checkera testu DataFamilyInstanceLHS z wyświetlonym równaniem rodziny typów danych. . . Błąd generowany przez kompilator w przypadku 4.1 po wprowadzeniu zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Ostrzeżenie generowane przez kompilator w przypadku 4.3 po wprowadzeniu zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . Błąd generowany przez kompilator w przypadku 4.5 po wprowadzeniu zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Przykład funkcji na poziomie typów ze zmiennymi uznawanymi za wykorzystywane. . . . . . . . . . . . . . . . . . . . . . . . . . . . . Przykład funkcji na poziomie typów ze zmiennymi zastąpionymi lub poprzedzonymi podkreślnikami. . . . . . . . . . . . . . . . . . . . . Fragment testu UnusedTyVarWarnings z równaniami wywołującymi ostrzeżenia lub zawierającymi zmienne poprzedzone podkreślnikiem. Ostrzeżenia generowany przez kompilator dla fragmentu 4.16 po wprowadzeniu zmian. . . . . . . . . . . . . . . . . . . . . . . . . . . Fragment testu NamedWildcardExplicitForall sprawdzającego, czy zmienne związane kwantyfikatorem nie zostają zamienione w wieloznaczniki. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Deklaracje, w których kompilacja kończyła się błędem, przy aktywnym rozszerzeniu NamedWildCards. . . . . . . . . . . . . . . . . . . 42 25 26 26 26 27 27 27 28 28 29 30 30 31 Dodatek A Płyta CD 43 DODATEK A. Zawartość katalogów na płycie: doc : elektroniczna wersja pracy dyplomowej oraz dwie prezentacje wygłoszone podczas seminarium dyplomowego src : repozytorium z kodem źródłowym aplikacji, z gałęzią master z dnia 2016-0131 i dwiema gałęziami trac-10839-consistent-ppr i trac-10982-unusedvars-in-tyfams, na których wykonywane były prace web : kopie źródeł elektronicznych umieszczonych w bibliografii Przełączenie się między gałęziami w repozytorium wymaga dodatkowego uaktualnienia submodułów używanych w projekcie komendą git submodule update --init. Szczegóły zawarte są w zasobie [3], którego kopię można znaleźć w katalogu web. Zbudowanie ghc wymaga przygotowania środowiska. Opis jak tego dokonać na poszczególnych platformach jest dostępny w [4]. Do katalogu web zostały skopiowane podstrony z instrukcjami dla systemów Windows, Linux i MacOS X. Sam proces budowania jest opisany w zasobie [17]. 44