Hallo Habr! Gehen Sie direkt zum Punkt. Im Moment lese ich "The Dragon Book" und entwickle einen Compiler für meine Programmiersprache namens Lolo (zu Ehren des Pinguins aus dem sowjetisch-japanischen Cartoon). Ich plane, innerhalb eines Jahres fertig zu sein, wenn nichts weh tut. Parallel dazu werde ich interessante Auszüge aus der Erfahrung mit Übersetzungen, dem Erstellen von Zwischencode, der Optimierung usw. veröffentlichen. Nun, heute werde ich Ihnen nur die Sprache vorstellen. Setz dich und geh.
Die Sprache ist kompiliert, zwingend, nicht objektorientiert, die Semantik wurde unverschämt von C abgeschrieben und mit vielen nützlichen Funktionen ergänzt. Beginnen wir mit ihnen.
Semantische Modifikationen
Sichere Zeiger
Sie haben vielleicht gerade über intelligente Zeiger von Rust nachgedacht, aber das sind sie nicht. In meiner Sprache wird die Sicherheit des Zugriffs auf den Speicher durch zwei Redewendungen gewährleistet. Erstens: das Fehlen einer Dereferenzierungsoperation von Zeigern. Beim Zugriff auf den deklarierten Zeiger wird stattdessen auf das Objekt selbst verwiesen. Das heißt, Sie können und sollten so schreiben:
int # pointer ~~ new int(5) int variable ~ pointer + 7
Die Variable Variable enthält jetzt die Nummer 12. Jetzt sehen Sie eine ungewohnte Syntax und sind vielleicht etwas ratlos, aber ich werde alles im Verlauf des Artikels erklären. Zweite Redewendung: Fehlende Operationen an Zeigern. Nochmals: Alle Operationen beim Zugriff auf Zeiger, einschließlich Zuweisung, Inkrementierung und Dekrementierung, werden für Objekte ausgeführt. Die einzige Operation, die sich direkt auf den Zeiger bezieht, ist die Zuweisung nach Adresse oder, wie ich es nenne, die Identifizierung. Im obigen Codebeispiel in der ersten Zeile handelt es sich um eine genaue Identifizierung. Jeder Zeiger kann nur auf die Adresse des bereits zugewiesenen Speicherbereichs gesetzt werden. Dies ist die neue zurückgegebene Operation. Sie können auch einen Zeiger auf die Adresse einer anderen Variablen setzen, die sogar auf dem Heap, sogar auf dem Stapel, zugewiesen ist. Hier ist ein Beispiel:
int variable ~ 5 int # pointer ~~ variable
Hier ist "~" die übliche Zuweisungsoperation. Sie können Zeiger auch mit einem speziellen Nullzeiger identifizieren. Es fungiert als Zeiger, der auf eine Nulladresse verweist. Nachdem die Operationen des Vergleichs und des Vergleichs der Identität (identische Adressen) mit null identifiziert wurden, ergeben sie true:
int # pointer ~~ null if (pointer = null) nop ;; true if (pointer == nul) nop ;; true
Hier ist "=" ein Vergleich von Werten, "==" ist ein Vergleich von Adressen, "nop" ist eine leere Operation und nach ";;" - Kommentar. Und ja, null ist die einzige Zeigeroperation, mit der ohne Überprüfung der Typkompatibilität möglich ist.
Daher können Zeiger nur dem zugewiesenen Speicher oder Nullbereichen zugewiesen und nicht an eine andere Stelle verschoben werden. Diese Maßnahmen schützen jedoch nicht vollständig vor Segmentierungsfehlerfehlern. Befolgen Sie dazu einfach die folgenden Schritte:
int # pointer1 ~~ new int(5) int # pointer2 ~~ pointer1 delete pointer1 int variable ~ pointer2 ;; segmentation fault!
Ich denke hier ist alles klar. Aber einen solchen Fehler zu machen, kann nur absichtlich gemacht werden und dann hart gearbeitet haben. Immerhin macht der Löschvorgang dasselbe wie der Garbage Collector, nur weniger sicher. Apropos ...
Müllsammler
Müllsammler - er ist auch ein Sammler in Lolo. Wahrscheinlich müssen Sie nicht erklären, was es ist. Ich kann nur sagen, dass es durch eine spezielle Option während der Kompilierung deaktiviert werden kann. Wir haben das Programm mit dem Collector getestet, alles funktioniert wie es sollte - Sie können die Option eingeben und versuchen, das Programm mithilfe der manuellen Speicherverwaltung zu optimieren.
Eingebaute Arrays
Obwohl ich sagte, dass die Semantik der Sprache von C abgeschrieben ist, sind die Unterschiede ziemlich signifikant. Hier sind Arrays Zeiger. Arrays haben ihre eigene Syntax und sichere Adressierung. Nein, nicht mit einer Reichweitenprüfung. Mit ihnen ist es im Prinzip schwierig, einen Laufzeitfehler zu bekommen. Dies liegt daran, dass jedes Array die Länge in der variablen Größe speichert, wie in Java, und bei jeder Indizierung aus dem Index ... gibt es den Rest der Division durch diese Größe! Eine dumme Entscheidung auf den ersten Blick, bis wir negative Indizes betrachten. Wenn Sie den Rest der Division von -1 durch die Länge des Arrays finden, erhalten Sie eine Zahl gleich Größe 1, dh das endlichste Element. Durch ein solches Manöver können wir nicht nur von Anfang an, sondern auch vom Ende des Arrays aus auf Indizes zugreifen. Ein weiterer Trick besteht darin, einen beliebigen primitiven Typ in das Array byte [] umzuwandeln. Aber wie kommt man zu einem Laufzeitfehler? Ich werde diese Frage für Sie als einfaches Rätsel hinterlassen.
Referenzen
Ich weiß nicht genau, ob der aktuelle C-Standard Links enthält, aber diese werden definitiv in Lolo sein. Vielleicht ist das Fehlen von Referenzen in früheren Versionen von C einer der Hauptgründe für Zeiger auf Zeiger. Sie werden benötigt, um Argumente an die Adresse zu übergeben und Werte von Funktionen ohne Kopieren zurückzugeben. Zeiger und Arrays können auch als Referenz übergeben werden (da beim Übergeben von Werten Arrays vollständig kopiert werden und Zeiger, die durch die Operation ~~ auf eine neue Position gesetzt wurden, diese nicht speichern).
Multithreading
Alles ist schöner und schöner. Ich bin schon in meine Sprache verliebt. Sein nächstes Hobby ist Multithreading. Ehrlich gesagt habe ich noch nicht vollständig entschieden, mit welchen Tools es bereitgestellt wird. Höchstwahrscheinlich das synchronisierte Schlüsselwort mit allen Eigenschaften von ala-Java und möglicherweise das gleichzeitige Schlüsselwort vor Nicht-Inline-Funktionen, was bedeutet, dass diese Funktionen in parallelen Threads ausgeführt werden.
Inline-Strings
Es handelt sich um Zeichenfolgen, nicht um Zeichenfolgenliterale wie in C ++. Jede Zeile hat ihre eigene Länge. Die Indizierung erfolgt mit dem Auffinden des Restes. Im Allgemeinen sind Zeichenfolgen in Lolo Zeichenarrays sehr ähnlich, mit der Ausnahme, dass Arrays keine Verkettung über "+", keine Animation über "*" und keine Vergleiche über "<" und ">" aufweisen. Und da es sich um Zeilen handelt, müssen wir die Zeichen erwähnen. Symbole in Lolo sind keine Zahlen, wie in C ++. Und sie enthalten nicht ein Byte, sondern 4 für DKOTI-Zeichen und 6 für UTF-Zeichen. Ich werde das nächste Mal über DKOTI sprechen, aber jetzt wissen Sie nur, dass Lolo Zeichen und Zeichenfolgen in zwei Codierungen unterstützt. Und ja, die Längeneigenschaft kann sogar Konstanten entnommen werden:
int len ~ "Hello, world!".length ;; len = 13
Boolescher Typ mit drei Werten
Die überwiegende Mehrheit der Programmiersprachen mit einem logischen Datentyp verwendet binäre Logik. Aber in Lolo wird es ternär oder besser gesagt unscharf ternär sein. Drei Werte: wahr - wahr, falsch - falsch und keine - nichts. Bisher habe ich in der Sprache der Operationen keine gefunden, die keine zurückgeben, aber ich erinnere mich an viele Beispiele aus der Praxis, als Flags mit drei Werten sehr nützlich wären. Musste Aufzählungen oder einen Integer-Typ verwenden. Muss nicht mehr. Das ist nur der Name dieses Typs, den ich nicht wählen kann. Am gebräuchlichsten ist „logisch“, aber zu lang. Andere Optionen sind "luk" zu Ehren von Jan Lukasevich, "brus" zu Ehren von N. P. Brusnetsov und "trit", aber genau genommen ist dieser Typ kein trit. Im Allgemeinen befindet sich die Umfrage am Ende des Artikels.
Listen zum Initialisieren von Strukturen und Listen
Wenn Sie nach dem Deklarieren einer Strukturvariablen das Zeichen ~ setzen und die eckigen Klammern öffnen, können Sie die Werte der Felder nacheinander oder in Form eines Wörterbuchs festlegen. Wenn Sie eine solche Prozedur mit einem Array ausführen, können Sie die Werte seiner Zellen nur ohne Wörterbuch festlegen. Es gibt nichts Besonderes zu erzählen, schauen Sie sich einfach den Code an:
struct { int i; real r; str s; } variable ~ [ i: 5, r: 3.14, s: "Hello!" ] int[5] arr ~ [ 1, 2, 3, 4, 5 ]
Gibt mehrere Werte von Funktionen zurück
Genau wie in Go! Sie können mehrere durch Kommas getrennte Variablennamen schreiben und ihnen alle von der Funktion zurückgegebenen Werte gleichzeitig zuweisen:
int, real function() { return 5, 3.14 } byte § { int i; real r i, r ~ function }
Module statt Header
Hier ist alles klar. Anstelle von C-schüchternen Headern - Module aus Java.
für (Auto Item: Array)
Wieder natives Java. Da wir Arrays mit Länge haben, ist es eine Sünde, den Ausdruck nicht für jedes zu verwenden.
Der Auswahloperator ist nicht nur für int
Ich weiß nichts über Sie, aber in C und C ++ bin ich schrecklich wütend darüber, dass die Switch-Case-Operation nicht für nicht ganzzahlige Variablen verwendet werden kann. Und die Syntax macht auch wütend. Hier in Pascal ist eine andere Sache. Und jetzt in Lolo:
case variable { "hello", "HELLO": nop "world": { nop; nop } "WORLD": nop }
Stromversorgungs- und Divisionsbetreiber
Und das ist von Python.
real r ~ 3.14 ** 2 int i ~ r // 3
Funktionsparameter-Tupel
Denken Sie daran, dass alle Operationen mit Zeigern in Lolo verboten sind, außer zur Identifizierung? Denken wir nun daran, wie Sie auf Funktionsparameter aus Parameterlisten variabler Länge zugreifen können. Sie müssen einen Zeiger auf das erste Element deklarieren und dann erhöhen, bis die Wahrheitsprüfung true zurückgibt. Sie können Lolo nicht erhöhen. Aber das ist okay. Schließlich wird die Liste der Parameter hier in Form eines Tupels fester (aufrufabhängiger) Länge mit index-safe wie in Arrays dargestellt. Sein Name ist "?" Die Typprüfung wird nur für Parameter durchgeführt, die in der Funktionsdefinition festgelegt sind. Die übrigen Parameter („Mehrpunktparameter“) werden auf einen beliebigen Typ reduziert, und bei einer unangenehmen Bewegung ist ihr Verhalten nicht definiert. Dennoch ist ein solches Tupel viel sicherer und bequemer als Makros in C.
void function(...) { if (?.size > 1) { int i ~ ?[0] real r ~ ?[1] } }
Numerische Intervalle
Und noch ein Zeichen - eine Familie von Intervalltypen (Bereich, Bereich, Bereich usw.). Sie werden durch zwei Ganzzahlen durch zwei Punkte (..) angegeben und können ein Array aus einem Array herausschneiden, eine Zeichenfolge aus einer Zeichenfolge, im Allgemeinen eine nützliche Sache, denke ich.
int[5] arr ~ [ 1, 2, 3, 4, 5 ] int[3] subarr = arr[1..3] ;; [ 2, 3, 4 ]
Im Bediener
Von Pascal. Funktioniert mit Strings, Arrays, Tupeln? und Bereiche.
int[5] arr ~ [ 1, 2, 3, 4, 5 ] if (4 in arr) nop
Funktionsparameter-Wörterbuch
Ehrlich gesagt bin ich bereits verwirrt, wie dieses Ding richtig genannt wird. Damit können Sie die Argumente von nicht reinen Funktionen direkt spezifizieren:
int pos = str_find(string, npos: -1)
Standardoptionen
Aus C ++. Hier ist nicht einmal ein Beispiel zu geben, und so ist alles klar.
Ausnahmen
Nun, und wo ohne sie?
try { raise SEGMENTATION_FAULT_EXCEPTION } except (Exception e) { print(e.rus) }
Kein bedingungsloser Sprung
Denn im Jahr 2019 ist die Verwendung des GOTO-Operators für den Tod ähnlich.
Syntax
Nun, ein kleines Gespräch über die Syntax. Wie Sie bemerkt haben, ist das Semikolon flach. Moderne Programmiersprachen kommen ohne diese Fehlerquelle sehr gut aus. Beispiele sind Python, Kotlin. Der Pfeiloperator (->) wird mit dem Punktoperator kombiniert. Beim Aufrufen von Funktionen ohne Argumente sind Klammern optional. Zeichenfolgen werden in Zahlen angegeben und umgekehrt. Logische und bitweise Operatoren werden kombiniert. Es gibt Funktionsmodifikatoren für die Tabellierung. Verschachtelte Funktionen type_of. Und vor allem - Mehrsprachigkeit. Ja, ich werde Schlüsselwörter, Eigenschaften von Zeichenfolgen und Arrays sowie alle Bezeichner der Standardbibliothek in allen Sprachen der internationalen Kommunikation duplizieren, nämlich Englisch, Russisch, Japanisch, Chinesisch, Spanisch, Portugiesisch, Arabisch, Französisch, Deutsch und Latein.
Tatsächlich beinhaltet all das nicht die Hälfte der Fähigkeiten von Lolo. Ich kann mich einfach nicht sofort an alle Funktionen erinnern. Ich werde hinzufügen, wenn der Compiler bereit ist.