Nowy język programowania Mash

Przez kilka lat próbowałem swoich sił w rozwijaniu własnego języka programowania. Chciałem stworzyć, moim zdaniem, możliwie najprostszy, w pełni funkcjonalny i wygodny język.

W tym artykule chcę nakreślić główne etapy mojej pracy, a na początek opisać stworzoną koncepcję języka i jej pierwszą implementację, nad którą obecnie pracuję.

Z góry zaznaczę, że cały projekt pisałem w Free Pascalu, bo... programy na nim można montować na ogromną liczbę platform, a sam kompilator tworzy bardzo zoptymalizowane pliki binarne (zbieram wszystkie komponenty projektu z flagą O2).

Środowisko wykonawcze języka

Przede wszystkim warto wspomnieć o maszynie wirtualnej, którą musiałem napisać, aby móc uruchamiać przyszłe aplikacje w moim języku. Zdecydowałem się wdrożyć architekturę stosu, być może dlatego, że był to najłatwiejszy sposób. Nie znalazłem ani jednego normalnego artykułu o tym, jak to zrobić w języku rosyjskim, więc po zapoznaniu się z materiałem w języku angielskim zabrałem się za projektowanie i pisanie własnego roweru. Następnie przedstawię moje „zaawansowane” pomysły i osiągnięcia w tej kwestii.

Implementacja stosu

Oczywiście na górze maszyny wirtualnej znajduje się stos. W mojej implementacji działa to blokowo. Zasadniczo jest to prosta tablica wskaźników i zmienna przechowująca indeks wierzchołka stosu.
Po zainicjowaniu tworzona jest tablica złożona z 256 elementów. Jeśli na stos zostanie wepchniętych więcej wskaźników, jego rozmiar zwiększy się o kolejne 256 elementów. W związku z tym podczas usuwania elementów ze stosu dostosowywany jest jego rozmiar.

Maszyna wirtualna korzysta z kilku stosów:

  1. Główny stos.
  2. Stos do przechowywania punktów zwrotu.
  3. Stos pojemników na śmieci.
  4. Spróbuj/złap/w końcu zablokuj stos obsługi.

Stałe i zmienne

To jest proste. Stałe są obsługiwane w oddzielnym, małym fragmencie kodu i będą dostępne w przyszłych aplikacjach za pośrednictwem adresów statycznych. Zmienne są tablicą wskaźników o określonej wielkości, dostęp do jej komórek odbywa się poprzez indeks - tj. adres statyczny. Zmienne można wypchnąć na górę stosu lub stamtąd odczytać. Właściwie, ponieważ Podczas gdy nasze zmienne zasadniczo przechowują wskaźniki do wartości w pamięci VM, w języku dominuje praca ze wskaźnikami ukrytymi.

Śmieciarz

W mojej maszynie wirtualnej jest to półautomatyczne. Te. sam deweloper decyduje, kiedy wezwać śmieciarza. Nie działa przy użyciu zwykłego licznika wskaźników, jak w Pythonie, Perlu, Ruby, Lua itp. Realizuje się to poprzez system znaczników. Te. gdy zmiennej ma zostać przypisana wartość tymczasowa, wskaźnik do tej wartości jest dodawany do stosu modułu zbierającego elementy bezużyteczne. W przyszłości kolekcjoner szybko przegląda przygotowaną już listę wskazówek.

Obsługa bloków try/catch/finally

Jak w każdym współczesnym języku, obsługa wyjątków jest ważnym elementem. Rdzeń maszyny wirtualnej jest opakowany w blok try..catch, który może powrócić do wykonywania kodu po przechwyceniu wyjątku poprzez umieszczenie informacji o nim na stosie. W kodzie aplikacji można zdefiniować bloki kodu try/catch/finally, określając punkty wejścia w punktach catch (obsługa wyjątków) i Final/end (koniec bloku).

Wielowątkowość

Jest obsługiwany na poziomie maszyny wirtualnej. Jest prosty i wygodny w użyciu. Działa bez systemu przerwań, więc kod powinien zostać wykonany odpowiednio w kilku wątkach kilka razy szybciej.

Biblioteki zewnętrzne dla maszyn wirtualnych

Bez tego nie da się obejść. VM obsługuje import, podobnie jak jest to zaimplementowane w innych językach. Możesz napisać część kodu w Mash i część kodu w językach ojczystych, a następnie połączyć je w jeden.

Tłumacz z wysokiego poziomu języka Mash na kod bajtowy dla maszyn wirtualnych

Język pośredni

Aby szybko napisać tłumacz ze złożonego języka na kod VM, najpierw opracowałem język pośredni. Rezultatem był straszny spektakl przypominający asembler, którego nie ma sensu tutaj rozważać. Powiem tylko, że na tym poziomie tłumacz przetwarza większość stałych i zmiennych, oblicza ich adresy statyczne oraz adresy punktów wejścia.

Architektura tłumacza

Nie wybrałem najlepszej architektury do wdrożenia. Tłumacz nie buduje drzewa kodów, jak robią to inni tłumacze. Patrzy na początek konstrukcji. Te. jeśli analizowany fragment kodu wygląda jak „while <warunek>:”, to oczywiste jest, że jest to konstrukcja pętli while i musi zostać przetworzona jako konstrukcja pętli while. Coś w rodzaju złożonej obudowy przełącznika.

Dzięki takiemu rozwiązaniu architektonicznemu tłumacz okazał się niezbyt szybki. Jednak łatwość jego modyfikacji znacznie wzrosła. Dodałam niezbędne struktury szybciej, niż moja kawa zdążyła wystygnąć. Pełne wsparcie OOP zostało wdrożone w niecały tydzień.

Optymalizacja kodu

Tutaj oczywiście można było to zaimplementować lepiej (i zostanie to wdrożone, ale później, jak tylko się za to weźmiemy). Jak dotąd optymalizator wie tylko, jak odciąć nieużywany kod, stałe i import z zestawu. Ponadto kilka stałych o tej samej wartości jest zastępowanych przez jeden. To wszystko.

Miazga językowa

Podstawowe pojęcia języka

Główną ideą było opracowanie możliwie najbardziej funkcjonalnego i prostego języka. Myślę, że produkcja poradziła sobie ze swoim zadaniem z przytupem.

Bloki kodu, procedury i funkcje

Wszystkie konstrukcje w języku otwiera się dwukropkiem. : i są zamykane przez operatora zakończenia.

Procedury i funkcje są deklarowane odpowiednio jako proc i func. Argumenty podano w nawiasach. Wszystko jest jak w większości innych języków.

Operator powrót możesz zwrócić wartość z funkcji, operatora złamać umożliwia wyjście z procedury/funkcji (jeśli znajduje się ona poza pętlami).

Przykładowy kod:

...

func summ(a, b):
  return a + b
end

proc main():
  println(summ(inputln(), inputln()))
end

Obsługiwane projekty

  • Pętle: for..end, while..end, aż do..end
  • Warunki: if..[else..]koniec, przełącznik..[case..end..][else..]koniec
  • Metody: proc <nazwa>():... koniec, func <nazwa>():... koniec
  • Etykieta i goto: <nazwa>:, skok <nazwa>
  • Wyliczenia wyliczeniowe i tablice stałe.

Zmienne

Tłumacz może je określić automatycznie lub jeśli programista napisze var przed ich zdefiniowaniem.

Przykłady kodu:

a ?= 10
b ?= a + 20

var a = 10, b = a + 20

Obsługiwane są zmienne globalne i lokalne.

OOP

No cóż, doszliśmy do najsmaczniejszego tematu. Mash obsługuje wszystkie paradygmaty programowania obiektowego. Te. klasy, dziedziczenie, polimorfizm (w tym dynamiczny), dynamiczna automatyczna refleksja i introspekcja (pełna).

Bez zbędnych ceregieli, lepiej po prostu podać przykłady kodu.

Prosta klasa i praca z nią:

uses <bf>
uses <crt>

class MyClass:
  var a, b
  proc Create, Free
  func Summ
end

proc MyClass::Create(a, b):
  $a = new(a)
  $b = new(b)
end

proc MyClass::Free():
  Free($a, $b)
  $rem()
end

func MyClass::Summ():
  return $a + $b
end

proc main():
  x ?= new MyClass(10, 20)
  println(x->Summ())
  x->Free()
end

Wyjdzie: 30.

Dziedziczenie i polimorfizm:

uses <bf>
uses <crt>

class MyClass:
  var a, b
  proc Create, Free
  func Summ
end

proc MyClass::Create(a, b):
  $a = new(a)
  $b = new(b)
end

proc MyClass::Free():
  Free($a, $b)
  $rem()
end

func MyClass::Summ():
  return $a + $b
end

class MyNewClass(MyClass):
  func Summ
end

func MyNewClass::Summ():
  return ($a + $b) * 2
end

proc main():
  x ?= new MyNewClass(10, 20)
  println(x->Summ())
  x->Free()
end

Wyjdzie: 60.

A co z dynamicznym polimorfizmem? Tak, to jest refleksja!:

uses <bf>
uses <crt>

class MyClass:
  var a, b
  proc Create, Free
  func Summ
end

proc MyClass::Create(a, b):
  $a = new(a)
  $b = new(b)
end

proc MyClass::Free():
  Free($a, $b)
  $rem()
end

func MyClass::Summ():
  return $a + $b
end

class MyNewClass(MyClass):
  func Summ
end

func MyNewClass::Summ():
  return ($a + $b) * 2
end

proc main():
  x ?= new MyClass(10, 20)
  x->Summ ?= MyNewClass::Summ
  println(x->Summ())
  x->Free()
end

Wyjdzie: 60.

Poświęćmy teraz chwilę na introspekcję prostych wartości i klas:

uses <bf>
uses <crt>

class MyClass:
  var a, b
end

proc main():
  x ?= new MyClass
  println(BoolToStr(x->type == MyClass))
  x->rem()
  println(BoolToStr(typeof(3.14) == typeReal))
end

Wypisze: prawda, prawda.

Informacje o operatorach przypisania i jawnych wskaźnikach

Operator ?= służy do przypisania zmiennej wskaźnika do wartości w pamięci.
Operator = zmienia wartość w pamięci za pomocą wskaźnika ze zmiennej.
A teraz trochę o wyraźnych wskaźnikach. Dodałem je do języka, aby istniały.
@<zmienna> — przyjmuje jawny wskaźnik do zmiennej.
?<zmienna> — pobierz zmienną za pomocą wskaźnika.
@= — przypisz wartość do zmiennej poprzez jawny wskaźnik do niej.

Przykładowy kod:

uses <bf>
uses <crt>

proc main():
  var a = 10, b
  b ?= @a
  PrintLn(b)
  b ?= ?b
  PrintLn(b)
  b++
  PrintLn(a)
  InputLn()
end

Wypisze: jakąś liczbę, 10, 11.

Spróbuj..[złap..][w końcu..]zakończ

Przykładowy kod:

uses <bf>
uses <crt>

proc main():
  println("Start")
  try:
    println("Trying to do something...")
    a ?= 10 / 0
  catch:
    println(getError())
  finally:
    println("Finally")
  end
  println("End")
  inputln()
end

Plany na przyszłość

Ciągle patrzę i patrzę na GraalVM i Truffle. Moje środowisko wykonawcze nie ma kompilatora JIT, więc pod względem wydajności obecnie konkuruje tylko z Pythonem. Mam nadzieję, że uda mi się zaimplementować kompilację JIT w oparciu o GraalVM lub LLVM.

magazyn

Możesz bawić się rozwojem i samodzielnie śledzić projekt.

Strona
Repozytorium na GitHubie

Dziękuję za przeczytanie do końca, jeśli tak się stało.

Źródło: www.habr.com

Dodaj komentarz