Tworzenie użytecznych języków JVM: przegląd
Opublikowany: 2022-03-11Istnieje kilka możliwych powodów stworzenia języka, z których niektóre nie są od razu oczywiste. Chciałbym je przedstawić wraz z podejściem do stworzenia języka dla wirtualnej maszyny Javy (JVM) wykorzystującego w jak największym stopniu istniejące narzędzia. W ten sposób zmniejszymy wysiłek programistyczny i zapewnimy znany użytkownikowi łańcuch narzędzi, ułatwiając przyjęcie naszego nowego języka programowania.
W tym artykule, pierwszym z serii, przedstawię przegląd strategii i różnych narzędzi związanych z tworzeniem naszego własnego języka programowania dla JVM. w kolejnych artykułach zagłębimy się w szczegóły implementacji.
Dlaczego warto tworzyć własny język JVM?
Istnieje już nieskończona liczba języków programowania. Po co więc zawracać sobie głowę tworzeniem nowego? Jest na to wiele możliwych odpowiedzi.
Przede wszystkim istnieje wiele różnych rodzajów języków: czy chcesz stworzyć język programowania ogólnego przeznaczenia (GPL) czy specyficzny dla domeny? Pierwszy rodzaj obejmuje języki takie jak Java czy Scala: języki przeznaczone do pisania wystarczająco przyzwoitych rozwiązań dużego zestawu problemów. Języki specyficzne dla domeny (DSL) zamiast tego koncentrują się na bardzo dobrym rozwiązywaniu określonego zestawu problemów. Pomyśl o HTML lub Latex: możesz rysować na ekranie lub generować dokumenty w Javie, ale byłoby to kłopotliwe, dzięki tym DSLom możesz tworzyć dokumenty bardzo łatwo, ale są one ograniczone do tej konkretnej domeny.
Być może więc istnieje zestaw problemów, nad którymi pracujesz bardzo często i dla których stworzenie DSL może mieć sens. Język, który sprawi, że będziesz bardzo produktywny, jednocześnie rozwiązując te same problemy w kółko.
Być może zamiast tego chcesz stworzyć GPL, ponieważ miałeś nowe pomysły, na przykład reprezentowanie relacji jako obywateli pierwszej klasy lub reprezentowanie kontekstu.
Wreszcie, możesz chcieć stworzyć nowy język, ponieważ jest fajny, fajny i ponieważ w trakcie tego będziesz się dużo uczyć.
Faktem jest, że jeśli celujesz w JVM, możesz uzyskać użyteczny język przy mniejszym nakładzie pracy, ponieważ:
- Wystarczy wygenerować kod bajtowy, a Twój kod będzie dostępny na wszystkich platformach, na których jest JVM
- Będziesz mógł wykorzystać wszystkie biblioteki i frameworki istniejące dla JVM
Tak więc koszt tworzenia języka jest znacznie obniżony na JVM i sensowne może być tworzenie nowych języków w scenariuszach, które byłyby nieekonomiczne poza JVM.
Czego potrzebujesz, aby można było z niego korzystać?
Jest kilka narzędzi, których absolutnie potrzebujesz, aby używać swojego języka - wśród tych narzędzi są parser i kompilator (lub interpreter). To jednak nie wystarczy. Aby twój język był naprawdę użyteczny w praktyce, musisz dostarczyć wiele innych elementów łańcucha narzędzi, być może integrując się z istniejącymi narzędziami.
Idealnie chcesz mieć możliwość:
- Zarządzaj odniesieniami do kodu skompilowanego dla JVM z innych języków
- Edytuj pliki źródłowe w swoim ulubionym IDE z podświetlaniem składni, identyfikacją błędów i automatycznym uzupełnianiem
- Chcesz mieć możliwość kompilacji plików przy użyciu swojego ulubionego systemu kompilacji: maven, gradle lub innych
- Chcesz mieć możliwość pisania testów i uruchamiania ich w ramach rozwiązania Continuous-Integration
Jeśli możesz to zrobić, przyswojenie Twojego języka będzie znacznie łatwiejsze.
Jak więc możemy to osiągnąć? W dalszej części posta przyjrzymy się różnym fragmentom, których potrzebujemy, aby było to możliwe.
Parsowanie i kompilacja
Pierwszą rzeczą, którą musisz zrobić, aby przekształcić pliki źródłowe w programie, jest ich parsowanie, uzyskując reprezentację drzewa składni abstrakcyjnego (AST) informacji zawartych w kodzie. W tym momencie będziesz musiał zweryfikować kod: czy występują błędy składniowe? Błędy semantyczne? Musisz je wszystkie znaleźć i zgłosić użytkownikowi. Jeśli wszystko pójdzie gładko, nadal musisz rozwiązać problemy z symbolami. Na przykład, czy „List” odnosi się do java.util.List czy java.awt.List ? Kiedy wywołujesz przeciążoną metodę, którą wywołujesz? Na koniec musisz wygenerować kod bajtowy dla swojego programu.
Tak więc od kodu źródłowego do skompilowanego kodu bajtowego są trzy główne fazy:
- Budowanie AST
- Analizowanie i przekształcanie AST
- Tworzenie kodu bajtowego z AST
Przyjrzyjmy się szczegółowo tym fazom.
Budowanie AST : parsowanie jest rodzajem rozwiązanego problemu. Istnieje wiele frameworków, ale sugeruję użycie ANTLR. Jest dobrze znany, dobrze utrzymany i ma kilka funkcji, które ułatwiają określanie gramatyk (obsługuje mniej rekurencyjnych reguł - nie musisz tego rozumieć, ale bądź wdzięczny, że tak!).
Analizowanie i przekształcanie AST : napisanie systemu typów, walidacja i rozwiązywanie symboli może być wyzwaniem i wymagać sporo pracy. Sam ten temat wymagałby osobnego postu. Na razie pomyśl, że jest to część twojego kompilatora, której poświęcisz większość wysiłku.
Wytworzenie kodu bajtowego z AST : ta ostatnia faza nie jest w rzeczywistości taka trudna. Powinieneś mieć rozwiązane symbole w poprzedniej fazie i przygotować teren tak, abyś w zasadzie mógł przetłumaczyć pojedyncze węzły twojego przekształconego AST na jeden lub kilka instrukcji kodu bajtowego. Struktury sterujące mogą wymagać dodatkowej pracy, ponieważ będziesz tłumaczyć swoje pętle for, przełączniki, ifs i tak dalej w sekwencji warunkowych i bezwarunkowych skoków (tak, pod twoim pięknym językiem nadal będzie kilka goto). Musisz dowiedzieć się, jak JVM działa wewnętrznie, ale faktyczna implementacja nie jest taka trudna.
Integracja z innymi językami
Kiedy uzyskasz dominację nad światem dla swojego języka, cały kod zostanie napisany wyłącznie przy użyciu tego języka. Jednak jako krok pośredni Twój język będzie prawdopodobnie używany wraz z innymi językami JVM. Być może ktoś zacznie pisać kilka zajęć lub małe moduły w Twoim języku w ramach większego projektu. Rozsądne jest oczekiwanie, że będzie można mieszać kilka języków JVM. Więc jak to wpływa na twoje narzędzia językowe?
Musisz wziąć pod uwagę dwa różne scenariusze:
- Twój język i pozostali żyją w modułach skompilowanych osobno
- Twój język i inne żyją w tych samych modułach i są kompilowane razem
W pierwszym scenariuszu Twój kod wymaga jedynie użycia skompilowanego kodu napisanego w innych językach. Na przykład niektóre zależności, takie jak Guava lub moduły w tym samym projekcie, mogą być kompilowane osobno. Ten rodzaj integracji wymaga dwóch rzeczy: po pierwsze, powinieneś być w stanie zinterpretować pliki klas utworzone przez inne języki, aby rozwiązać na nie symbole i wygenerować kod bajtowy do wywołania tych klas. Drugi punkt jest zwierciadlany w stosunku do pierwszego: inne moduły mogą chcieć ponownie wykorzystać kod napisany w twoim języku po jego skompilowaniu. Teraz zwykle nie stanowi to problemu, ponieważ Java może wchodzić w interakcje z większością plików klas. Jednak nadal możesz napisać pliki klas, które są poprawne dla JVM, ale nie mogą być wywoływane z Javy (na przykład dlatego, że używasz identyfikatorów, które nie są poprawne w Javie).

Drugi scenariusz jest bardziej skomplikowany: załóżmy, że masz klasę A zdefiniowaną w kodzie Java i klasę B napisaną w twoim języku. Załóżmy, że te dwie klasy odwołują się do siebie (na przykład A może rozszerzyć B, a B może przyjąć A jako parametr dla tej samej metody). Teraz chodzi o to, że kompilator Java nie może przetworzyć kodu w twoim języku, więc musisz dostarczyć mu plik klasy dla klasy B. Jednak aby skompilować klasę B, musisz wstawić referencje do klasy A. Więc co musisz zrobić, to posiadanie czegoś w rodzaju częściowego kompilatora Javy, który biorąc pod uwagę plik źródłowy Javy jest w stanie go zinterpretować i stworzyć jego model, którego można użyć do skompilowania swojej klasy B. Zauważ, że wymaga to umiejętności parsowania kodu Javy (używając coś takiego jak JavaParser) i rozwiązywać symbole. Jeśli nie masz pojęcia, od czego zacząć, spójrz na java-symbol-solver.
Narzędzia: Gradle, Maven, Test Frameworks, CI
Dobrą wiadomością jest to, że możesz sprawić, aby fakt, że używają modułu napisanego w Twoim języku, był całkowicie przejrzysty dla użytkownika, tworząc wtyczkę dla gradle lub maven. Możesz poinstruować system kompilacji, aby kompilował pliki w twoim języku programowania. Użytkownik będzie nadal uruchamiał mvn compile lub gradle assemble i nie zauważy żadnej różnicy.
Zła wiadomość jest taka, że pisanie wtyczek Mavena nie jest łatwe: dokumentacja jest bardzo uboga, niezrozumiała i w większości przestarzała lub po prostu niewłaściwa . Tak, to nie brzmi pocieszająco. Nie napisałem jeszcze wtyczek do gradle, ale wydaje się to o wiele łatwiejsze.
Pamiętaj, że powinieneś również rozważyć, w jaki sposób testy mogą być uruchamiane za pomocą systemu kompilacji. W celu wsparcia testów powinieneś pomyśleć o bardzo podstawowym frameworku do testów jednostkowych i powinieneś zintegrować go z systemem kompilacji, aby uruchomienie testu maven szukało testów w twoim języku, kompilowało je i uruchamiało raportując wynik do użytkownika.
Radzę spojrzeć na dostępne przykłady: jednym z nich jest wtyczka Maven do języka programowania Turin.
Po zaimplementowaniu każdy powinien być w stanie łatwo kompilować pliki źródłowe napisane w Twoim języku i używać ich w usługach ciągłej integracji, takich jak Travis.
Wtyczka IDE
Wtyczka do IDE będzie najbardziej widocznym narzędziem dla Twoich użytkowników i czymś, co znacznie wpłynie na postrzeganie Twojego języka. Dobra wtyczka może pomóc użytkownikowi w nauce języka, zapewniając inteligentne autouzupełnianie, błędy kontekstowe i sugerowane refaktoryzacje.
Teraz najczęstszą strategią jest wybranie jednego IDE (zazwyczaj Eclipse lub IntelliJ IDEA) i opracowanie dla niego określonej wtyczki. Jest to prawdopodobnie najbardziej złożony element twojego łańcucha narzędzi. Dzieje się tak z kilku powodów: po pierwsze, nie możesz rozsądnie ponownie wykorzystać pracy, którą poświęcisz na tworzenie wtyczki dla jednego IDE dla innych. Twoje Eclipse i twoja wtyczka IntelliJ będą całkowicie oddzielne. Drugą kwestią jest to, że tworzenie wtyczek IDE nie jest czymś bardzo powszechnym, więc nie ma zbyt wiele dokumentacji, a społeczność jest niewielka. Oznacza to, że będziesz musiał poświęcić dużo czasu na samodzielne zastanawianie się. Osobiście opracowałem wtyczki do Eclipse i IntelliJ IDEA. Moje pytania na forach Eclipse pozostawały bez odpowiedzi przez miesiące lub lata. Na forach IntelliJ miałem więcej szczęścia i czasami otrzymywałem odpowiedź od deweloperów. Jednak baza użytkowników twórców wtyczek jest mniejsza, a API jest bardzo bizantyjskie. Przygotuj się na cierpienie.
Istnieje alternatywa dla tego wszystkiego i jest to użycie Xtext. Xtext to framework do tworzenia wtyczek dla Eclipse, IntelliJ IDEA i sieci. Narodził się na Eclipse i niedawno został rozszerzony o obsługę innych platform, więc nie ma w tym zbyt dużego doświadczenia, ale może być wartą rozważenia alternatywą. Powiem wprost: jedynym sposobem na stworzenie bardzo dobrej wtyczki jest stworzenie jej przy użyciu natywnego API każdego IDE. Jednak z Xtext możesz mieć coś całkiem przyzwoitego za ułamek wysiłku - po prostu nadajesz to składni swojego języka i otrzymujesz błędy składni/uzupełnianie za darmo. Nadal musisz zaimplementować rozdzielczość symboli i twarde części, ale jest to bardzo interesujący punkt wyjścia; jednak twarde bity to integracja z bibliotekami specyficznymi dla platformy w celu rozwiązywania symboli Java, więc tak naprawdę nie rozwiąże to wszystkich twoich problemów.
Wnioski
Istnieje wiele sposobów na utratę potencjalnych użytkowników, którzy wykazali zainteresowanie Twoim językiem. Przyswojenie nowego języka jest wyzwaniem, ponieważ wymaga nauki go i dostosowania naszych nawyków rozwojowych. Zmniejszając w jak największym stopniu zużycie i wykorzystując ekosystem znany już użytkownikom, możesz powstrzymać użytkowników przed poddaniem się, zanim nauczą się i zakochają w Twoim języku.
W idealnym scenariuszu użytkownik mógłby sklonować prosty projekt napisany w Twoim języku i zbudować go za pomocą standardowych narzędzi (Maven lub Gradle) bez zauważania różnicy. Jeśli chce edytować projekt, może go otworzyć w swoim ulubionym edytorze, a wtyczka pomoże mu wskazać błędy i zapewni sprytne uzupełnienia. Jest to scenariusz zupełnie inny niż konieczność wymyślenia sposobu wywołania kompilatora i edytowania plików za pomocą notatnika. Ekosystem wokół Twojego języka może naprawdę wiele zmienić, a obecnie można go zbudować przy rozsądnym wysiłku.
Radzę być kreatywnym w swoim języku, ale nie w swoich narzędziach. Zmniejsz początkowe trudności, z jakimi muszą się mierzyć ludzie, aby zaadaptować Twój język, stosując znane standardy.
Udanego projektowania języka!
Dalsza lektura na blogu Toptal Engineering:
- Jak podejść do pisania tłumacza od podstaw
