Tytuł tego artykułu odzwierciedla pytanie, które ciągle słyszę na forach lub w e-mailach, które otrzymuję.
Myślę, że wszyscy ciekawscy programiści zadali je przynajmniej raz. To normalne, że jesteśmy zafascynowani tym, jak działają języki programowania. Niestety, większość odpowiedzi, które czytamy są bardzo akademickie lub teoretyczne. Inne zawierają zbyt wiele szczegółów implementacyjnych. Po ich przeczytaniu nadal zastanawiamy się, jak to wszystko działa w praktyce.
Więc zamierzamy na to pytanie odpowiedzieć. Tak, zobaczymy jaki jest proces tworzenia własnego pełnego języka z kompilatorem do niego i co nie.
Przegląd
Większość osób, które chcą się dowiedzieć jak „stworzyć język programowania” w rzeczywistości szuka informacji jak zbudować kompilator. Chcą zrozumieć mechanikę, która pozwala na wykonanie nowego języka programowania.
Kompilator jest podstawowym elementem układanki, ale stworzenie nowego języka programowania wymaga czegoś więcej:
1) Język musi być zaprojektowany: twórca języka musi podjąć pewne fundamentalne decyzje dotyczące paradygmatów, które mają być użyte, i składni języka
2) Kompilator musi zostać stworzony
3) Standardowa biblioteka musi zostać zaimplementowana
4) Narzędzia wspierające, takie jak edytory i systemy budowania, muszą zostać dostarczone
Zobaczmy bardziej szczegółowo, co pociąga za sobą każdy z tych punktów.
Projektowanie języka programowania
Jeśli chcesz po prostu napisać swój własny kompilator, aby dowiedzieć się, jak te rzeczy działają, możesz pominąć tę fazę. Możesz po prostu wziąć podzbiór istniejącego języka lub wymyślić jego prostą odmianę i zacząć. Jeśli jednak masz w planach stworzenie własnego języka programowania, będziesz musiał się nad tym zastanowić.
Myślę o projektowaniu języka programowania jako podzielonym na dwie fazy:
- Faza „big-picture”
- Faza „refinement”
W pierwszej fazie odpowiadamy na podstawowe pytania dotyczące naszego języka.
- Jakiego paradygmatu wykonania chcemy użyć? Czy będzie on imperatywny czy funkcyjny? A może oparty na maszynach stanów lub regułach biznesowych?
- Czy chcemy statyczne czy dynamiczne typowanie?
- Do jakiego rodzaju programów ten język będzie najlepszy? Czy będzie on używany do małych skryptów czy dużych systemów?
- Co jest dla nas najważniejsze: wydajność? Czytelność?
- Czy chcemy, aby był on podobny do istniejącego języka programowania? Czy będzie on skierowany do programistów C, czy też łatwy do nauczenia dla tych, którzy pochodzą z Pythona?
- Czy chcemy, aby działał na konkretnej platformie (JVM, CLR)?
- Jakiego rodzaju możliwości metaprogramowania chcemy wspierać, jeśli w ogóle? Makra? Szablony? Refleksja?
W drugiej fazie będziemy rozwijać język w miarę jak będziemy go używać. Będziemy napotykać na problemy, na rzeczy, które są bardzo trudne lub niemożliwe do wyrażenia w naszym języku i w końcu będziemy go ewoluować. Druga faza może nie być tak wspaniała jak pierwsza, ale jest to faza, w której ciągle dostrajamy nasz język, aby był użyteczny w praktyce, więc nie powinniśmy jej lekceważyć.
Budowanie kompilatora
Budowanie kompilatora jest najbardziej ekscytującym krokiem w tworzeniu języka programowania. Kiedy mamy już kompilator, możemy właściwie powołać nasz język do życia. Kompilator pozwala nam na rozpoczęcie zabawy z językiem, używanie go i identyfikację tego, czego brakuje nam w początkowym projekcie. Pozwala zobaczyć pierwsze rezultaty. Trudno jest pobić radość z wykonania pierwszego programu napisanego w naszym nowym języku programowania, bez względu na to, jak prosty może być ten program.
Ale jak zbudować kompilator?
Jak wszystko, co złożone, robimy to w następujących krokach:
- Budujemy parser: parser jest częścią naszego kompilatora, która bierze tekst naszych programów i rozumie, jakie polecenia wyrażają. Rozpoznaje wyrażenia, deklaracje, klasy i tworzy wewnętrzne struktury danych, aby je reprezentować. Reszta parsera będzie pracować z tymi strukturami danych, a nie z oryginalnym tekstem
- (opcjonalnie) Tłumaczymy drzewo parsowania na Abstrakcyjne Drzewo Składni. Zazwyczaj struktury danych produkowane przez parser są nieco niskiego poziomu, ponieważ zawierają wiele szczegółów, które nie są kluczowe dla naszego kompilatora. Z tego powodu chcemy często uporządkować struktury danych w coś nieco bardziej wysokopoziomowego
- Rozwiązujemy symbole. W kodzie piszemy rzeczy takie jak
a + 1
. Nasz kompilator musi się dowiedzieć do czego odnosi sięa
. Czy jest to pole? Czy jest to zmienna? Czy jest to parametr metody? Badamy kod, aby odpowiedzieć na to pytanie - Sprawdzamy poprawność drzewa. Musimy sprawdzić, czy programista nie popełnił błędów. Czy próbuje on zsumować boolean i int? Albo uzyskuje dostęp do nieistniejącego pola? Musimy wygenerować odpowiednie komunikaty o błędach
- Generujemy kod maszynowy. W tym momencie tłumaczymy kod na coś, co maszyna może wykonać. Może to być właściwy kod maszynowy lub kod bajtowy dla jakiejś maszyny wirtualnej
- (opcjonalnie) Wykonujemy łączenie. W niektórych przypadkach musimy połączyć kod maszynowy utworzony dla naszych programów z kodem statycznych bibliotek, które chcemy dołączyć, w celu wygenerowania pojedynczego pliku wykonywalnego
Czy zawsze potrzebujemy kompilatora? Nie. Możemy go zastąpić innymi środkami do wykonywania kodu:
- Możemy napisać interpreter: interpreter jest zasadniczo programem, który wykonuje kroki 1-4 kompilatora, a następnie bezpośrednio wykonuje to, co jest określone przez Abstrakcyjne Drzewo Składni
- Możemy napisać transpiler: transpiler zrobi to, co jest określone w krokach 1-4, a następnie wyprowadzi jakiś kod w jakimś języku, dla którego mamy już kompilator (na przykład C++ lub Java)
Te dwie alternatywy są całkowicie poprawne i często ma sens wybór jednego z tych dwóch, ponieważ wymagany wysiłek jest zwykle mniejszy.
Pisaliśmy artykuł wyjaśniający, jak napisać transpiler. Rzuć na niego okiem, jeśli chcesz zobaczyć praktyczny przykład z kodem.
W tym artykule wyjaśniamy bardziej szczegółowo różnicę między kompilatorem a interpreterem.
Biblioteka standardowa dla twojego języka programowania
Każdy język programowania musi robić kilka rzeczy:
- Drukowanie na ekranie
- Dostęp do systemu plików
- Używanie połączeń sieciowych
- Tworzenie GUI
To są podstawowe funkcjonalności do interakcji z resztą systemu. Bez nich język jest w zasadzie bezużyteczny. Jak możemy zapewnić te funkcjonalności? Poprzez stworzenie standardowej biblioteki. Będzie to zbiór funkcji lub klas, które można wywołać w programach napisanych w naszym języku programowania, ale które będą napisane w jakimś innym języku. Na przykład, wiele języków posiada biblioteki standardowe napisane przynajmniej częściowo w języku C.
Biblioteka standardowa może wtedy zawierać znacznie więcej. Na przykład klasy do reprezentowania głównych kolekcji, takich jak listy i mapy, lub do przetwarzania popularnych formatów, takich jak JSON lub XML. Często będzie zawierać zaawansowane funkcjonalności do przetwarzania ciągów znaków i wyrażeń regularnych.
Innymi słowy, pisanie biblioteki standardowej to dużo pracy. Nie jest to efektowne, nie jest koncepcyjnie tak interesujące jak pisanie kompilatora, ale nadal jest to podstawowy składnik, który sprawia, że język programowania jest opłacalny.
Istnieją sposoby na uniknięcie tego wymogu. Jednym z nich jest sprawienie, by język działał na jakiejś platformie i umożliwienie ponownego użycia biblioteki standardowej innego języka. Na przykład, wszystkie języki działające na JVM mogą po prostu ponownie wykorzystać bibliotekę standardową Javy.
Narzędzia wspierające nowy język programowania
Aby uczynić język użytecznym w praktyce, często musimy napisać kilka narzędzi wspierających.
Najbardziej oczywistym jest edytor. Specjalistyczny edytor z kolorowaniem składni, sprawdzaniem błędów i autouzupełnianiem jest dziś niezbędny, aby każdy programista był produktywny.
Ale dziś programiści są rozpieszczani i oczekują wszelkiego rodzaju innych narzędzi wspomagających. Na przykład, debugger może być naprawdę przydatny, aby poradzić sobie z paskudnym błędem. Albo system budowania podobny do maven lub gradle może być czymś, o co użytkownicy będą pytać później.
Na samym początku edytor może wystarczyć, ale w miarę jak baza użytkowników będzie się powiększać, złożoność projektów będzie rosnąć i więcej narzędzi wspomagających będzie potrzebnych. Miejmy nadzieję, że w tym czasie znajdzie się społeczność chętna do pomocy w ich tworzeniu.
Podsumowanie
Tworzenie języka programowania jest procesem, który wydaje się tajemniczy dla wielu programistów. W tym artykule staraliśmy się pokazać, że jest to po prostu proces. Jest to fascynujące i niełatwe, ale da się to zrobić.
Możesz chcieć zbudować język programowania z różnych powodów. Jednym dobrym powodem jest zabawa, innym nauka, jak działają kompilatory. Twój język może być bardzo użyteczny lub nie, w zależności od wielu czynników. Jednak jeśli będziesz się dobrze bawił i/lub uczył podczas jego tworzenia, to warto zainwestować w to trochę czasu.
I oczywiście będziesz mógł się pochwalić swoim kolegom programistom.
Jeśli chcesz dowiedzieć się więcej o tworzeniu języka spójrz na inne zasoby, które stworzyliśmy: naucz się jak budować języki.
Możesz być również zainteresowany niektórymi z naszych artykułów: