Neue Programmiersprache Mash

Mehrere Jahre lang habe ich versucht, meine eigene Programmiersprache zu entwickeln. Meiner Meinung nach wollte ich eine möglichst einfache, voll funktionsfähige und benutzerfreundliche Sprache schaffen.

In diesem Artikel möchte ich die Hauptphasen meiner Arbeit hervorheben und zunächst das erstellte Konzept der Sprache und ihre erste Implementierung beschreiben, an der ich derzeit arbeite.

Lassen Sie mich vorab sagen, dass ich das gesamte Projekt in Free Pascal geschrieben habe, weil... Programme darauf können für eine große Anzahl von Plattformen zusammengestellt werden, und der Compiler selbst erzeugt sehr optimierte Binärdateien (ich sammle alle Komponenten des Projekts mit der O2-Flagge).

Sprachlaufzeit

Zunächst lohnt es sich, über die virtuelle Maschine zu sprechen, die ich schreiben musste, um zukünftige Anwendungen in meiner Sprache auszuführen. Ich habe mich vielleicht für die Implementierung einer Stack-Architektur entschieden, weil das der einfachste Weg war. Ich habe keinen einzigen normalen Artikel darüber auf Russisch gefunden, also habe ich mich, nachdem ich mich mit dem englischsprachigen Material vertraut gemacht hatte, daran gemacht, mein eigenes Fahrrad zu entwerfen und zu schreiben. Als nächstes werde ich meine „fortgeschrittenen“ Ideen und Entwicklungen in dieser Angelegenheit vorstellen.

Stack-Implementierung

Offensichtlich befindet sich an der Spitze der VM der Stapel. In meiner Implementierung funktioniert es in Blöcken. Im Wesentlichen handelt es sich hierbei um ein einfaches Array aus Zeigern und einer Variablen zum Speichern des Indexes der obersten Ebene des Stapels.
Bei der Initialisierung wird ein Array mit 256 Elementen erstellt. Werden weitere Zeiger auf den Stapel geschoben, erhöht sich dessen Größe um die nächsten 256 Elemente. Dementsprechend wird beim Entfernen von Elementen aus dem Stapel dessen Größe angepasst.

Die VM verwendet mehrere Stacks:

  1. Hauptstapel.
  2. Ein Stapel zum Speichern von Rückgabepunkten.
  3. Müllsammelstapel.
  4. Try/catch/finally Block-Handler-Stack.

Konstanten und Variablen

Dieser ist einfach. Konstanten werden in einem separaten kleinen Codeabschnitt behandelt und stehen in zukünftigen Anwendungen über statische Adressen zur Verfügung. Variablen sind ein Array von Zeigern einer bestimmten Größe, der Zugriff auf seine Zellen erfolgt über den Index – d.h. Statische Adresse. Variablen können an die Spitze des Stapels verschoben oder von dort gelesen werden. Eigentlich, weil Während unsere Variablen im Wesentlichen Zeiger auf Werte im VM-Speicher speichern, dominiert in der Sprache die Arbeit mit impliziten Zeigern.

Müllsammler

In meiner VM ist es halbautomatisch. Diese. Der Entwickler selbst entscheidet, wann der Garbage Collector aufgerufen wird. Es funktioniert nicht mit einem regulären Zeigerzähler wie in Python, Perl, Ruby, Lua usw. Die Umsetzung erfolgt über ein Markersystem. Diese. Wenn einer Variablen ein temporärer Wert zugewiesen werden soll, wird dem Stapel des Garbage Collectors ein Zeiger auf diesen Wert hinzugefügt. Zukünftig geht der Collector schnell die bereits vorbereitete Liste der Zeiger durch.

Umgang mit try/catch/finally-Blöcken

Wie in jeder modernen Sprache ist die Ausnahmebehandlung ein wichtiger Bestandteil. Der VM-Kern ist in einen try..catch-Block eingeschlossen, der nach dem Abfangen einer Ausnahme zur Codeausführung zurückkehren kann, indem er einige Informationen darüber auf den Stapel schiebt. Im Anwendungscode können Sie Codeblöcke vom Typ „try/catch/finally“ definieren und dabei Einstiegspunkte bei „catch“ (Ausnahmehandler) und „finally/end“ (Ende des Blocks) angeben.

Multithreading

Es wird auf VM-Ebene unterstützt. Es ist einfach und bequem zu verwenden. Es funktioniert ohne Interrupt-System, sodass der Code in mehreren Threads jeweils um ein Vielfaches schneller ausgeführt werden sollte.

Externe Bibliotheken für VMs

Darauf kann man nicht verzichten. VM unterstützt Importe, ähnlich wie es in anderen Sprachen implementiert ist. Sie können einen Teil des Codes in Mash und einen Teil des Codes in Muttersprachen schreiben und sie dann zu einer einzigen verknüpfen.

Übersetzer von der höheren Mash-Sprache in Bytecode für VMs

Mittelstufe

Um schnell einen Übersetzer aus einer komplexen Sprache in VM-Code zu schreiben, habe ich zunächst eine Zwischensprache entwickelt. Das Ergebnis war ein Assembler-artiges Schreckensspektakel, das hier nicht näher betrachtet werden soll. Ich möchte nur sagen, dass der Übersetzer auf dieser Ebene die meisten Konstanten und Variablen verarbeitet, ihre statischen Adressen und die Adressen der Einstiegspunkte berechnet.

Übersetzerarchitektur

Ich habe nicht die beste Architektur für die Implementierung ausgewählt. Der Übersetzer erstellt keinen Codebaum, wie es andere Übersetzer tun. Er blickt auf den Anfang der Struktur. Diese. Wenn der zu analysierende Code wie „while <Bedingung>:“ aussieht, ist es offensichtlich, dass es sich um ein While-Schleifenkonstrukt handelt und als While-Schleifenkonstrukt verarbeitet werden muss. So etwas wie ein komplexes Schaltergehäuse.

Dank dieser architektonischen Lösung erwies sich der Übersetzer als nicht sehr schnell. Allerdings ist die Einfachheit seiner Modifikation deutlich gestiegen. Ich habe die nötigen Strukturen schneller hinzugefügt, als mein Kaffee abkühlen konnte. Die vollständige OOP-Unterstützung wurde in weniger als einer Woche implementiert.

Codeoptimierung

Hier hätte es natürlich besser umgesetzt werden können (und wird umgesetzt, aber später, sobald man dazu kommt). Bisher weiß der Optimierer nur, wie er ungenutzten Code, Konstanten und Importe aus der Assembly abschneidet. Außerdem werden mehrere Konstanten mit demselben Wert durch eine ersetzt. Das ist alles.

Mash-Sprache

Grundbegriff der Sprache

Die Hauptidee bestand darin, eine möglichst funktionale und einfache Sprache zu entwickeln. Ich denke, dass die Entwicklung ihre Aufgabe mit Bravour meistert.

Codeblöcke, Prozeduren und Funktionen

Alle Konstruktionen in der Sprache werden mit einem Doppelpunkt eröffnet. : und werden vom Betreiber geschlossen Ende.

Prozeduren und Funktionen werden als proc bzw. func deklariert. Die Argumente sind in Klammern aufgeführt. Alles ist wie in den meisten anderen Sprachen.

Operator Rückkehr Sie können einen Wert von einer Funktion oder einem Operator zurückgeben brechen ermöglicht Ihnen, die Prozedur/Funktion zu verlassen (wenn sie sich außerhalb der Schleifen befindet).

ример кода:

...

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

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

Unterstützte Designs

  • Schleifen: for..end, while..end, Until..end
  • Bedingungen: if..[else..]end, switch..[case..end..][else..]end
  • Methoden: proc <name>():... end, func <name>():... end
  • Label & gehe zu: <Name>:, springe zu <Name>
  • Enum-Aufzählungen und konstante Arrays.

Variablen

Der Übersetzer kann sie automatisch ermitteln, oder wenn der Entwickler var schreibt, bevor er sie definiert.

Codebeispiele:

a ?= 10
b ?= a + 20

var a = 10, b = a + 20

Es werden globale und lokale Variablen unterstützt.

OOP

Nun, wir sind beim köstlichsten Thema angelangt. Mash unterstützt alle objektorientierten Programmierparadigmen. Diese. Klassen, Vererbung, Polymorphismus (einschließlich dynamischer), dynamischer automatischer Reflexion und Selbstbeobachtung (vollständig).

Ohne weitere Umschweife ist es besser, nur Codebeispiele zu nennen.

Eine einfache Klasse und die Arbeit damit:

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

Wird ausgegeben: 30.

Vererbung und Polymorphismus:

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

Wird ausgegeben: 60.

Was ist mit dynamischem Polymorphismus? Ja, das ist Reflexion!:

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

Wird ausgegeben: 60.

Nehmen wir uns nun einen Moment Zeit, um nach einfachen Werten und Klassen zu suchen:

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

Gibt aus: wahr, wahr.

Über Zuweisungsoperatoren und explizite Zeiger

Der Operator ?= wird verwendet, um einer Variablen einen Zeiger auf einen Wert im Speicher zuzuweisen.
Der =-Operator ändert einen Wert im Speicher mithilfe eines Zeigers einer Variablen.
Und nun ein wenig zu expliziten Hinweisen. Ich habe sie der Sprache hinzugefügt, damit sie existieren.
@<Variable> – Nimm einen expliziten Zeiger auf eine Variable.
?<Variable> – eine Variable per Zeiger abrufen.
@= – Weisen Sie einer Variablen einen Wert zu, indem Sie einen expliziten Zeiger darauf setzen.

ример кода:

uses <bf>
uses <crt>

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

Gibt aus: eine Zahl, 10, 11.

Versuchen Sie es..[fangen..][endlich..]Ende

ример кода:

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

Pläne für die Zukunft

Ich schaue immer wieder nach GraalVM & Truffle. Meine Laufzeitumgebung verfügt über keinen JIT-Compiler und ist daher leistungstechnisch derzeit nur mit Python konkurrenzfähig. Ich hoffe, dass ich die JIT-Kompilierung basierend auf GraalVM oder LLVM implementieren kann.

Repository

Sie können mit den Entwicklungen spielen und das Projekt selbst verfolgen.

Webseite
Repository auf GitHub

Vielen Dank, dass Sie bis zum Ende gelesen haben.

Source: habr.com

Kommentar hinzufügen