Die Entwurfsprinzipien des Nimony-Compilers für die zukünftige Nim 3.0 werden vorgestellt

Im Rahmen der Entwicklung der Programmiersprache Nim 3.0 entsteht der neue Compiler Nimony, dessen grundlegendes Entwurfsprinzip die Vorhersehbarkeit der Laufzeit im schlimmsten Fall (Worst Case Execution Time, WCET) ist. Diese Anforderung ergibt sich aus dem Fokus auf Systeme mit harter Echtzeit, bei denen undeterministisches Verhalten nicht akzeptabel ist. Infolgedessen schließt die Architektur von Nimony den Einsatz von JIT-Compilern und tracing garbage collectors aus, da deren Aktionen unvorhersehbare Verzögerungen verursachen können.

Um Vorhersehbarkeit zu erreichen, werden primitive Datentypen (Ganzzahlen, Zeichen) direkt auf Maschinenwörter und Bytes der entsprechenden Architektur abgebildet. Zusammengesetzte Typen (Strukturen, Objekte) werden ohne den Einsatz von indirekter Adressierung (Indirection) gebildet und befinden sich direkt im Stack oder innerhalb anderer Datenstrukturen. Dieser Ansatz minimiert den Overhead und gewährleistet eine transparentere Übereinstimmung zwischen dem Quellcode und dem generierten Maschinencode.

Im Bereich des automatischen Speichermanagements (MM) schlägt Nimony einen anderen Weg ein als die Vielfalt der Optionen, die in Nim 2.0 verfügbar sind, und bietet nur einen einheitlichen standardisierten Modus: „mm:atomicArc“. Dieser Modus basiert auf einer Referenzzählung mit atomaren Operationen, ergänzt durch die Semantik der Bewegung (Move Semantics) und dem Aufruf von Destruktoren beim Zerstören von Objekten, was den Ansatz mit den Praktiken von Rust und modernem C++ näher bringt.

Eine der wichtigsten Neuerungen ist die klare Trennung von Objekten in azyklische und potenziell zyklische. Standardmäßig werden Objekte als azyklisch (.acyclic) betrachtet, was ein neues Verhalten darstellt. Für Datentypen, deren Instanzen zyklische Verweise bilden können, ist eine explizite Anmerkung mit der Pragmanotwendigkeit .cyclic erforderlich. Es wird angemerkt, dass ein neuer Algorithmus zur Verwaltung von zyklischen Verweisen entwickelt wird, jedoch ist seine Verwendbarkeit in der Industrie derzeit nicht garantiert. Ein Vorteil von MM, das auf Destruktoren basiert, ist seine Komponierbarkeit: Das Management von Ressourcen, die freigegeben werden müssen (zum Beispiel Dateideskriptoren, Netzwerksockets, Kanäle), wird nahtlos durch die Destruktoren der entsprechenden Typen integriert.

Der Umgang mit Fehlerbehandlungen in Nimony hat sich erheblich verändert. Der Autor Nim zeigt sich unzufrieden mit traditionellen Ausnahmebehandlungsmechanismen und deren Emulation durch algebraische Datentypen (Summentypen). Stattdessen wird das Konzept vorgeschlagen, den Fehlerstatus direkt im Datenelement zu integrieren. Beispiele hierfür sind: die Darstellung von Fehlern in Ein- und Ausgabeströmen durch einen speziellen Status, die Verwendung von NaN für Gleitkommazahlen oder low(int) für ungültige Ganzzahlwerte. In Fällen, in denen ein Objekt den Fehlerstatus nicht kapseln kann, wird vorgeschlagen, eine thread-lokale Variable zur Signalisierung zu verwenden.

Dennoch bleibt der traditionelle Ausnahmebehandlungsmechanismus von Nim erhalten, jedoch mit einer wichtigen Klarstellung: Jede Prozedur, die eine Ausnahme auslösen kann, muss nun zwingend mit der Pragmas {.raises.} annotiert werden. Diese Anforderung zielt darauf ab, potenzielle nicht-lokale Kontrollübergänge eindeutig zu kennzeichnen.

Als Alternative oder Ergänzung wird ein neuer aufzählbarer Typ ErrorCode eingeführt. Dieser Typ ist typsicher und erfordert die umfassende Behandlung aller möglichen Varianten (analog zu 'case' für Aufzählungen). ErrorCode wurde so entworfen, dass er auf Standardfehlercodes verschiedener Systeme und Protokolle, wie POSIX errno, Windows API Fehlercodes und HTTP-Statuscodes, abgebildet werden kann. Ziel ist es, die Fehlerbehandlung zwischen verschiedenen Bibliotheken zu vereinheitlichen und eine direkte Übertragung von Systemfehlern (z. B. "Speicherplatz erschöpft") in die entsprechenden Statuscodes (z. B. HTTP 507) ohne zusätzliche Umwandlungen zu ermöglichen. Die Verwendung von ErrorCode ermöglicht es auch, Fehler zu behandeln und weiterzugeben, ohne Speicher im Heap zu beanspruchen, was für die Verarbeitung von Situationen mit Speichermangel (OOM) entscheidend ist.

Die Verarbeitung von Situationen mit Speicherauslastung (Out of Memory, OOM) in Nimony weicht von der gängigen Praxis der Programmbeendigung im Fehlerfall ab („die on OOM“). Stattdessen wird ein Mechanismus bereitgestellt, der es der Anwendung ermöglicht, weiterzuarbeiten. Container und Speicherzuteilungsoperationen, die eine Anforderung nicht erfüllen können, rufen einen überschriebbaren Handler, oomHandler, auf. Die Standardimplementierung protokolliert die Größe der fehlgeschlagenen Anfrage in einer thread-lokalen Variablen und erlaubt die Fortsetzung der Ausführung. Der Zustand des Speichermangels für den aktuellen Thread kann durch den Aufruf von threadOutOfMem() überprüft werden.

Der Entwickler kann eine eigene Implementierung von oomHandler bereitstellen, beispielsweise für die Protokollierung oder das Notabschalten der Anwendung, falls ein solches Verhalten gewünscht ist. Ein wichtiger Aspekt ist die Handhabung der Konstruktion von Referenzobjekten (ref object), die aufgrund von OOM fehlschlagen kann. In Nimony kann das Ergebnis solcher Operationen (zum Beispiel über new oder ähnliche Konstruktoren) nil sein, und der Compiler erzwingt die Verarbeitung dieses Falls ähnlich wie bei optionalen Typen (Option), um so Nullzeiger-Dereferenzierungsfehler zu vermeiden. Im Kontext von Verfahren, die mit {.raises.} annotiert sind, kann der Rückgabewert nil automatisch in ErrorCode.OutOfMemError umgewandelt werden.

Der Mechanismus der generischen Programmierung in Nimony hat im Vergleich zu Nim 2.0 Fortschritte gemacht. Die wesentliche Verbesserung besteht darin, dass die vollständige Typprüfung von generischem Code jetzt bereits beim Definitionstage erfolgt, anstatt nur beim Instanziieren mit konkreten Typen. Dies soll dazu beitragen, Fehler früher im Kompilierungsprozess zu erkennen, informativere Fehlermeldungen bereitzustellen und die Unterstützung durch Entwicklungsumgebungen (IDEs) zu verbessern, insbesondere die Code-Autovervollständigung.

Die Konzepte, die bereits in Nim vorhanden sind, behalten ihre Rolle als Mechanismus zur statischen Beschreibung der Anforderungen an Typparameter in generischen Funktionen und Typen. Sie ermöglichen es, formell anzugeben, welche Operationen oder Eigenschaften ein Typ erfüllen muss, um in diesem generischen Kontext verwendet werden zu können.

Nimony verfolgt die Vereinheitlichung von asynchroner und multithreaded Programmierung unter der einheitlichen Struktur von spawn. Die Entscheidung, ob eine durch spawn gestartete Aufgabe im gleichen Thread (asynchron) oder in einem separaten Thread aus dem Pool (multithreaded) ausgeführt wird, trifft der Scheduler zur Laufzeit. Dies stellt bestimmte Anforderungen an die Argumente, die an spawn übergeben werden: Sie müssen threadsicher sein.

Die interne Implementierung des Wettbewerbsmodells basiert auf Fortsetzungen (Continuations), und der Compiler wird das Programm in den Fortsetzungsübergabestil (Continuation-Passing Style, CPS) umwandeln. Es wird angemerkt, dass die Konstruktion spawn nicht als integrierte Fähigkeit der Sprache, sondern als Compiler-Plugin realisiert ist.

Parallele Verarbeitung gilt als eine einfachere Aufgabe im Vergleich zur Konkurrenz. Für das Schreiben von rein parallelem Code, der auf Berechnungen ausgerichtet ist (z. B. Verarbeitung von Datenarrays), bietet Nimony spezielle Konstrukte wie parallele for-Schleifen, gekennzeichnet durch den Operator „||“. Dies ermöglicht die Implementierung paralleler Algorithmen, ohne dass Steuerflussvariablen (flow vars) erforderlich sind, was für wissenschaftliche Berechnungen oder Programmierung für GPUs nützlich sein kann.

Das Metaprogrammierungssystem von Nim, das für seine Makros bekannt ist, entwickelt sich in Nimony zu Compiler-Plugins. Plugins sind Code, der in native Anweisungen kompiliert wird und in einer späten Phase des Compilervorgangs, nach der Typüberprüfung, ausgeführt wird. Dies gibt den Plugins Zugang zu vollständigen Informationen über die Typen und die Semantik des analysierten Codes.

Es wurden verbesserte und benutzerfreundlichere APIs zur Entwicklung von Plugins versprochen. Die Verwendung des Zwischenformats NIF (Nim Intermediate Format) sollte zudem die Implementierung verschiedener Code-Transformationen erleichtern. Plugins können inkrementell und parallel ausgeführt werden, was die Kompilierungsleistung steigert.

Plugin-Typen:

  • Template-Plugins: Diese sind an bestimmte Templates gebunden und verarbeiten den Code, der mit deren Aufrufen verbunden ist.
  • Modul-Plugins: Sie erhalten als Eingabe den AST (Abstract Syntax Tree) des gesamten Moduls und müssen einen transformierten Baum zurückgeben. Die Konstruktion spawn ist ein Beispiel für die Implementierung über ein Modul-Plugin.
  • Iterator-Plugins: Diese sind ähnlich den Template-Plugins, werden jedoch auf Iteratoren angewendet.
  • Plugins für nominelle Typen: Sie können an bestimmte Datentypen gebunden werden und ersetzen den Mechanismus der „Makros zur Umformung von Termen“ (term rewriting macros) aus Nim. Dadurch sind Optimierungen möglich, wie die Eliminierung temporärer Objekte bei der Ausführung von Matrixoperationen.

Für die Dokumentation von Nimony gibt es eine Website, die vollständig von KI generiert und vom Autor von Nim auf ihre Richtigkeit überprüft wurde.

Quelle: opennet.ru

Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen 🔥 Zuverlässiges Webhosting mit DDoS-Schutz, VPS- und VDS-Server kaufen | ProHoster