Über mehrere Jahre hinweg habe ich versucht, meine eigenen Fähigkeiten in der Entwicklung einer Programmiersprache zu testen. Mein Ziel war es, eine Sprache zu schaffen, die meiner Meinung nach so einfach, funktional und benutzerfreundlich wie möglich ist.
In diesem Artikel möchte ich die wesentlichen Phasen meiner Arbeit beleuchten und zunächst das Konzept der Sprache sowie die erste Umsetzung, an der ich derzeit arbeite, beschreiben.
Vorab möchte ich erwähnen, dass ich das gesamte Projekt in Free Pascal geschrieben habe, da die Programme damit auf einer Vielzahl von Plattformen kompiliert werden können und der Compiler sehr optimierte Binaries erzeugt (ich kompiliere alle Projektbestandteile mit dem O2-Flag).
Die Ausführungsumgebung der Sprache
Zunächst sollte ich von der virtuellen Maschine erzählen, die ich für die Ausführung zukünftiger Anwendungen in meiner Sprache schreiben musste. Ich entschied mich für eine stackbasierte Architektur, da dies wohl die einfachste Lösung war. Ich konnte keinen einzigen brauchbaren Artikel auf Russisch finden, der mir dabei geholfen hätte, also habe ich, nachdem ich mich mit englischsprachigem Material vertraut gemacht hatte, mit der Planung und dem Schreiben meines eigenen 'Bikes' begonnen. Im Folgenden werde ich meine "fortschrittlichen" Ideen und Entwicklungen in diesem Bereich präsentieren.
Die Implementierung des Stacks
Offensichtlich basiert die VM auf einem Stack. In meiner Implementierung funktioniert er in Blöcken. Im Grunde handelt es sich um ein einfaches Array von Zeigern und einer Variablen zur Speicherung des Index des Stacktopps.
Bei der Initialisierung wird ein Array mit 256 Elementen erstellt. Wenn mehr Zeiger auf den Stack gelegt werden, wird die Größe um jeweils 256 Elemente erhöht. Entsprechend wird die Größe des Stacks reguliert, wenn Elemente entfernt werden.
In der VM werden mehrere Stacks verwendet:
- Der Hauptstack.
- Stack zur Speicherung von Rückgabepunkten.
- Der Stack des Garbage Collectors.
- Stack für try/catch/finally-Blöcke.
Konstanten und Variablen
Damit ist alles einfach. Konstanten werden von einem separaten kleinen Codeabschnitt verarbeitet und sind in zukünftigen Anwendungen unter statischen Adressen verfügbar. Variablen sind ein Array von Zeigern fester Größe, wobei der Zugriff auf seine Zellen über den Index erfolgt – also über eine statische Adresse. Variablen können an die Spitze des Stacks gelegt oder von dort gelesen werden. Tatsächlich speichern unsere Variablen im Wesentlichen Zeiger auf Werte im VM-Speicher, sodass im Sprachgebrauch vorwiegend mit impliziten Zeigern gearbeitet wird.
Garbage Collector
In meiner VM ist er halbautomatisch. Das heißt, der Entwickler entscheidet selbst, wann der Garbage Collector aufgerufen werden soll. Er funktioniert nicht über einen herkömmlichen Zeigerzähler, wie in Python, Perl, Ruby, Lua usw. sondern wird über ein Marker-System realisiert. Wenn einer Variablen ein temporärer Wert zugewiesen werden soll, wird der Zeiger auf diesen Wert im Stack des Garbage Collectors hinzugefügt. Der Collector durchläuft dann schnell die bereits vorbereitete Liste von Zeigern.
Bearbeitung von try/catch/finally-Blöcken
Wie in jeder modernen Sprache ist die Verarbeitung von Ausnahmen ein wesentlicher Bestandteil. Der Kern der VM ist in einen try..catch-Block eingebettet, der nach dem Auffangen einer Ausnahme zur Ausführung des Codes zurückkehren kann, indem er einige Informationen darüber auf dem Stack speichert. In Anwendungscode können try/catch/finally-Blöcke definiert werden, wobei die Eingangsstellen für catch (Ausnahmebehandler) und finally/end (Ende des Blocks) angegeben werden.
Multithreading
Es wird auf der Ebene der VM unterstützt. Das ist einfach und bequem zu verwenden. Es funktioniert ohne Interrupt-System, sodass der Code in mehreren Threads entsprechend um ein Vielfaches schneller ausgeführt werden kann.
Externe Bibliotheken für die VM
Das ist unvermeidlich. Die VM unterstützt Importe, ähnlich wie in anderen Sprachen. Man kann einen Teil des Codes in Mash und einen Teil in nativen Sprachen schreiben und sie dann zu einem Ganzen verbinden.
Übersetzer von einer höheren Sprache Mash in Bytecode für die VM
Zwischensprache
Um schnell einen Übersetzer von einer komplexen Sprache in Code für die VM zu erstellen, habe ich zunächst eine Zwischensprache entwickelt. Es entstand ein assemblerähnliches, erschreckendes Gebilde, dessen Betrachtung hier kaum sinnvoll ist. Ich kann nur sagen, dass auf dieser Ebene der Übersetzer die meisten Konstanten und Variablen verarbeitet, ihre statischen Adressen und Einstiegspunkte berechnet.
Architektur des Übersetzers
Ich wählte nicht die beste Architektur für die Implementierung. Der Übersetzer erstellt keinen Codebaum, wie es anderen Übersetzern zusteht. Er schaut auf den Anfang der Konstruktion. Das heißt, wenn der analysierte Code Abschnitt die Form «while :» hat, ist offensichtlich, dass es sich um eine while-Schleifen-Konstruktion handelt, die als solche zu behandeln ist. Irgendetwas ähnliches wie ein komplexes switch-case.
Dank dieser architektonischen Lösung ist der Compiler nicht besonders schnell geworden. Allerdings hat sich die Einfachheit seiner Erweiterung erheblich verbessert. Ich konnte die benötigten Konstrukte schneller hinzufügen, als mein Kaffee abkühlen konnte. Die vollständige Unterstützung der objektorientierten Programmierung wurde sogar in weniger als einer Woche umgesetzt.
Code-Optimierung
Es hätte natürlich auch besser umgesetzt werden können (und wird es auch, wenn ich dazu komme). Der Optimierer kann derzeit nur ungenutzten Code, Konstanten und Imports aus dem Build entfernen. Auch mehrere Konstanten mit dem gleichen Wert werden durch eine einzige ersetzt. Das ist alles.
Mash-Sprache
Das Hauptkonzept der Sprache
Die Hauptidee war es, eine möglichst funktionale und einfache Sprache zu entwickeln. Ich denke, dass die Entwicklung ihre Aufgabe hervorragend erfüllt.
Codeblöcke, Prozeduren und Funktionen
Alle Konstrukte in der Sprache beginnen mit einem Doppelpunkt : und enden mit dem Operator end.
Prozeduren und Funktionen werden als proc und func deklariert. In Klammern werden die Argumente aufgelistet. Ganz wie bei den meisten anderen Sprachen.
Mit dem Operator return kann ein Wert aus einer Funktion zurückgegeben werden, der Operator break erlaubt es, aus einer Prozedur/Funktion auszutreten (wenn er nicht in Schleifen steht).
Beispielcode:
...
func summ(a, b):
return a + b
end
proc main():
println(summ(inputln(), inputln()))
end
Unterstützte Konstrukte
- Schleifen: for..end, while..end, until..end
- Bedingungen: if..[else..]end, switch..[case..end..][else..]end
- Methoden: proc <Name>():… end, func <Name>():… end
- Label & goto: <Name>:, jump <Name>
- Enum für Aufzählungen und konstante Arrays.
Variablen
Der Compiler kann diese automatisch erkennen, oder der Entwickler kann var vor deren Definition schreiben.
Codebeispiele:
a ?= 10
b ?= a + 20
var a = 10, b = a + 20
Globale und lokale Variablen werden unterstützt.
OOP
Nun kommen wir zum spannendsten Thema. Die Sprache Mash unterstützt alle Paradigmen der objektorientierten Programmierung. Das heißt Klassen, Vererbung, Polymorphismus (einschließlich dynamischem), dynamische automatisierte Reflexion und vollständige Introspektion.
Ohne viele Worte werde ich einfach die Codebeispiele bringen.
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
Gibt aus: 30.
Vererbung und Polymorphismus:
uses
uses
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
Gibt aus: 60.
Wie sieht es mit dynamischem Polymorphismus aus? Das ist doch Reflexion!
uses
uses
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
Gibt aus: 60.
Lassen Sie uns jetzt einen Moment der Introspektion für einfache Werte und Klassen widmen:
uses
uses
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: true, true.
Über Zuweisungsoperatoren und explizite Zeiger.
Der Operator ?= dient dazu, einer Variablen einen Zeiger auf einen Wert im Speicher zuzuweisen.
Der Operator = ändert den Wert im Speicher über den Zeiger aus der Variablen.
Und nun etwas über explizite Zeiger. Ich habe sie in die Sprache aufgenommen, damit sie vorhanden sind.
@ — um einen expliziten Zeiger auf eine Variable zu erhalten.
?<переменная> — получить переменную по указателю.
@= — weisen Sie einen Wert einer Variablen durch einen direkten Verweis zu.
Beispielcode:
uses
uses
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.
Try..[catch..][finally..]end
Beispielcode:
uses
uses
proc main():
println("Start")
try:
println("Versuche etwas zu tun...")
a ?= 10 / 0
catch:
println(getError())
finally:
println("Endlich")
end
println("Ende")
inputln()
end
Zukunftspläne
Ich schaue mir GraalVM & Truffle an. Meine Laufzeitumgebung hat keinen JIT-Compiler, sodass sie in Bezug auf die Leistung höchstens mit Python konkurrieren kann. Ich hoffe, ich kann die JIT-Kompilierung auf Basis von GraalVM oder LLVM umsetzen.
Repository
Sie können mit den Entwicklungen experimentieren und das Projekt selbst verfolgen.
Danke, dass Sie bis zum Ende gelesen haben, falls Sie es getan haben.
Quelle: habr.com
