Benutzer:Kleinesfilmröllchen/Jakt (Programmiersprache)

Kleinesfilmröllchen/Jakt
Paradigmen: objektorientiert, generisch, systemnah, funktional
Erscheinungsjahr: 2022
Designer: Andreas Kling, JT und die Entwickler des SerenityOS-Projekts
Typisierung: stark, statisch, explizit und implizit
Beeinflusst von: Swift, Rust, C++
Betriebssystem: SerenityOS, Linux, Windows, macOS
Lizenz: 2-Klausel-BSD-Lizenz
serenityos/jakt

Jakt (schwedisch jakt ‚Jagd‘) ist eine systemnahe Multiparadigmen-Programmiersprache, die vom SerenityOS-Projekt entwickelt wird. Die Sprache sieht sich als auf Konzepten der Programmiersprachen Swift, Rust und C++ basierend und ist speziell auf den Einsatz im SerenityOS-Betriebssystem ausgelegt. Zentrale Elemente der Sprache sind Objektorientierung, Interoperabilität mit C++, Speicher- und Typsicherheit, Fehlerbehandlung zur Laufzeit, Compilezeit-Berechnung und generische Programmierung. Stand September 2022 befinden sich die nicht spezifizierte Sprache und der Compiler in einem experimentellen instabilen Zustand.

Geschichte

Bearbeiten

Im Mai 2022 kündigte Andreas Kling, Erfinder von SerenityOS und dessen de-facto-Projektleiter, die Programmiersprache Jakt in einem Blogpost an.[1] Er meinte, dass C++, die momentan in SerenityOS eingesetzte Programmiersprache, auf lange Sicht ungeeignet für das Projekt sei, da sie keine Speicher- oder Typsicherheit bietet. Kling begann deshalb zwei Wochen vor der Ankündigung, mit JT, einem Rust-Kernentwickler, eine SerenityOS-eigene Programmiersprache zu implementieren. Die erste Implementierung wurde als C++-Transpiler in Rust geschrieben und sollte als sogenannter Bootstrap-Compiler dienen, um einen in Jakt selbst geschriebenen Compiler zu kompilieren. Nach der Ankündigung wurde das Projekt der SerenityOS-Organisation untergeordnet, deren Projektgemeinschaft den Compiler seither gemeinsam entwickelt.

Um sich auf die Vervollständigung und Stabilisierung des Selbstcompilers zu konzentrieren, wurde der Funktionsumfang ab DATUM! vorerst eingefroren. Darauf folgend am DATUM! 2022 wurde der Bootstrap-Compiler aus dem Projekt entfernt.

Etymologie

Bearbeiten

Jakt im Sinne des schwedischen Wortes, was dem deutschen „Jagd“ (in Bedeutung und Abstammung) entspricht, ist die offizielle von Kling angegebene Wortherkunft. Weiterhin sind die vier Buchstaben die Initialen der ursprünglichen beiden Entwickler (AK und JT). Außerdem ähnelt die Aussprache dem (englisch, deutsch und schwedischen) Yak, welches in der SerenityOS-Gemeinschaft ein verbreitetes Meme darstellt.

Eigenschaften

Bearbeiten

Da Jakt keine Sprachspezifikation aufweist, entstammen viele dieser Informationen den Beispielen[2], dem Jakt-Informationsdokument[3] und der Compilerimplementierung selbst[4].

Bezüglich grundlegender Syntax ähnelt Jakt allen Sprachen der C-Familie, maßgebliche Abweichungen sind z.B. das Fehlen von Klammern um Kontrollausdrücke und die Möglichkeit zum Auslassen von Semikolons als Anweisungstrennung. Das untenstehende Beispiel zeigt einen Vergleich zwischen Jakt und beinahe äquivalentem C++-Code. Unter den üblichen Kontrollstrukturen ist insbesondere match hervorzuheben, welches mit einer Rust-ähnlichen Syntax ein aus funktionalen Programmiersprachen bekanntes mächtiges Pattern Matching implementiert.

// Einfaches Jakt-Programm, welches BubbleSort implementiert.
// https://github.com/SerenityOS/jakt/blob/main/samples/basics/bubble_sort.jakt

function main() {
    mut v = [25, 13, 8, 1, 9, 22, 50, 2]
    bubble_sort(values: v)
    mut i = 0
    while i < v.size() as! i64 {
        println("{}", v[i])
        ++i
    }
}

function bubble_sort(mut values: [i64]) {
    mut i = 0
    while i < values.size() as! i64 - 1 {
        mut j = 0
        while j < (values.size() as! i64) - i - 1 {
            if values[j] > values[j + 1] {
                let tmp = values[j]
                values[j] = values[j + 1]
                values[j + 1] = tmp
            }
            ++j
        }
        ++i
    }
}
// Äquivalentes BubbleSort-Programm in C/C++.
// Beachte, dass dieses Programm idiomatischer geschrieben werden könnte,
// dies jedoch zur Verdeutlichung des Vergleichs nicht der Fall ist.

#include <stdio.h>

void bubble_sort(long*, size_t);

int main() {
	long v[] = {25, 13, 8, 1, 9, 22, 50, 2};
	size_t v_size = sizeof(v) / sizeof(long);
	bubble_sort(v, v_size);
	long i = 0;
	while (i < (long)v_size) {
		printf("%ld\n", v[i]);
		++i;
	}
}

void bubble_sort(long* values, size_t size) {
	long i = 0;
	while (i < ((long)size) - 1) {
		long j = 0;
		while (j < ((long)size) - i - 1) {
			if (values[j] > values[j + 1]) {
				long const tmp = values[j];
				values[j] = values[j + 1];
				values[j + 1] = tmp;
			}
			++j;
		}
		++i;
	}
}

Typsystem

Bearbeiten

Typen werden in Jakt üblicherweise inferiert und müssen daher nicht vom Programmierer angegeben werden. Dies gilt insbesondere für Rückgabewerte, aber nicht für Funktionsparameter. Die Schlüsselwörter let und mut bezeichnen konstante bzw. veränderbare Variablen. Unter den standardmäßigen Typen sind unter anderem numerische Typen (i32, u8, f64 usw.), Containertypen (Listen [T], Mengen {T}, Wörterbücher [T: U]) Tupeltypen (T, U, V) und optionale Typen T?, da Jakt standardmäßig kein Null erlaubt.

let a = "Text" // Typ String
let b = 4 as! u8 // Typ u8 (ohne die Konvertierung wäre es i64)
let c: i64 = b // Fehler: i64 und u8 sind nicht kompatibel
let a = "Anderer Text" // Fehler: a ist nicht veränderbar

mut maybe_int: u16? = None
maybe_int = Some(78) // bei `mut` ist die Zuweisung erlaubt
println("{}", maybe_int!) // -> 78
maybe_int = None
println("{}", maybe_int ?? 5) // -> 5

let array = [1, 4, 3] // Typ [i64]
println("{}", x[1]) // -> 4

let x = ("a", 2, true) // Typ (String, i64, bool)
println("{}", x.1) // -> 2

Jakt besitzt eine Art der Objektorientierung, wie sie z.B. von Java und C++ bekannt ist. Der Unterschied zwischen class (Klassen) und struct (Strukturen) geht jedoch wesentlich weiter, als von den meisten Sprachen bekannt: Klassen sind wie in Swift standardmäßig referenzgezählt und eignen sich für die Art von GUI-Programmierung, die den Großteil der SerenityOS-Codebase ausmacht. Kopieren einer Klasse umfasst normalerweise lediglich das Kopieren des Pointers und Erhöhen der Referenzzahl. Strukturen hingegen sind nicht referenzgezählt und werden vollständig kopiert, können sich daher auch auf dem Stack befinden. Ansonsten können Klassen wie Strukturen Felder und Methoden enthalten, außerdem ist Einfachvererbung innerhalb der beiden Kategorien möglich. (Beispiel: Vererbung)

class Person {
    // Öffentliche Felder (standardmäßig privat)
    public name: String
    public age: i64
    
    // Statische Methode
    // restricted erlaubt es Friend, auf diese Funktion zuzugreifen.
    restricted(Friend) function tell_secret() => "shhh!"
    
    // Instanzmethode, die die Instanz modifiziert
    public function birthday(mut this) => {
        ++ this.age
        // Obiges kann auch mit dieser Kurzschreibweise ausgedrückt werden:
        ++ .age
    }
}

class Friend {
    name: String
    
    public function ask_secret() => Person::tell_secret()

    public function edit_person(mut someone_else: Person) => someone_else.name = "Blank"
    public function edit_vector(mut vector: Vector) => vector.x = 0
}

struct Vector {
    // In Strukturen sind Felder standardmäßig öffentlich.
    x: f64
    y: f64
}

mut p = Person(name: "Jane", age: 100) // benutzt den impliziten Standardkonstruktor
println("{} {}", p.name, p.age) // -> Jane 100

// Sichtbarkeit von Funktionen:
Friend::ask_secret() // erlaubt
Person::tell_secret() // Fehler: tell_secret ist privat

// Klassen haben Referenzsemantik:
Friend::edit_person(someone_else: p) // Kopiert die Referenz, nicht das Objekt.
println("{}", p.name) // -> Blank

// Strukturen haben Wertesemantik:
let vector = Vector(x: 300, y: 400)
Friend::edit_vector(vector) // möglich obwohl vector nicht änderbar ist!
println("{}", vector.x) // -> 300

Zwei weitere Arten der benutzerdefinierten Typen existieren: Typaliase mittels type sowie Vereinigungs- bzw. Summentypen mittels enum. Summentypen sind Werttypen wie struct und können regulär Methoden enthalten. (Beispiel: Type)

// Für ein ausführliches Beispiel, das u.a. Optional reimplementiert, siehe
// https://github.com/SerenityOS/jakt/blob/main/samples/enums/simple_match.jakt

// Aus C/C++ bekannt
enum Simple {
    A
    B
    C
}

// "Echter" Summentyp
enum SimpleWithType {
    A(i32)
    B(u32)

    // Summentypen können wie Klassen und Strukturen Methoden enthalten
    function get_type(this) => match this {
        A(signed) => signed as! i64
        B(unsigned) => unsigned as! i64
    }
}

// Die enthaltenen Typen können auch Strukturen sein.
enum SimpleWithStructType {
    A (
        a: i32
        b: u32
    )
    B(i32)
}

// Rekursive Summentypen können explizit Referenzsemantik erhalten, falls nötig.
boxed enum Foo {
    Var(var: Simple)
    IndexedStruct(expr: Foo)

    function is_mutable(this) -> bool => match this {
        Var(var) => true
        IndexedStruct(expr) => expr.is_mutable()
    }
}

Eine Besonderheit bei der Deklaration und dem Aufruf von Funktionen und Methoden, die in den Beispielen bereits zum Tragen kam, sind benannte und unbenannte Parameter. Ein Parameter muss standardmäßig beim Aufruf mit seinem Namen beschriftet werden. (erlaubt das Umsortierung?) Dies dient explizit und ausschließlich der Lesbarkeit und Wartbarkeit. Ausnahmen für diese Regel sind als anon (anonym) deklarierte formale Parameter sowie this-Parameter in Methoden.

(Beispiel)

Jakt ermöglicht generische Programmierung mittels Typparametern auf Klassen, Funktionen und Typdefinitionen. Die möglichen konkreten Typen können mittels Traits eingeschränkt werden, was zur Compilezeit geprüft wird. Traits ähneln dem gleichnamigen Rust-Konzept, sind jedoch ausschließlich dazu da, Trait-Methoden eines Typs statisch zu binden. Dies unterscheidet sich von Jakts Klassenvererbung, wo dynamische Bindung durch virtuelle Methoden möglich ist.

(Beispiel)

Die Möglichkeiten, Prüfungen und Berechnungen zur Compilezeit durchzuführen, geht über Traits hinaus, da Jakt mit comptime Compilezeitprogrammierung ermöglicht. Die Sprachfunktionalität ist hier wie üblich eingeschränkt, soll jedoch mit der Zeit erweitert werden.

// Diese Funktion wird zur Laufzeit des Programms nicht aufgerufen!
// Das Ergebnis wird bereits ausgewertet, wenn das Programm kompiliert.
comptime fibonacci(anon value: i64) -> i64 {
    if value < 2 {
        return value
    }

    return fibonacci(value - 1) + fibonacci(value - 2)
}

function main() {
    println("fibonacci(16) = {}", fibonacci(16)) // -> fibonacci(16) = 987
}

Sicherheit

Bearbeiten

Ein Jakt-Programm ist standardmäßig speicher- und typsicher, auch da der Compiler bestimmte Laufzeitprüfungen für z. B. Arraygrenzen oder Überlauf einfügt. Insbesondere unterliegen mathematische Operationen verschiedenen Überprüfungen, die mit Bibliotheksfunktionen auf mehrere Arten umgangen werden können (beispielsweise Sättigung an den Randwerten eines Ganzzahltyps statt Überlauf). Bezüglich aller Typen sind Typkonvertierungen (as) nur in eingeschränktem Rahmen möglich, außerdem werden solche Konvertierungen standardmäßig zur Laufzeit geprüft. Sollen Sicherheitsprüfungen absichtlich umgangen werden, z. B. um im Kernel in beliebigen Speicher schreiben zu können, ist ein unsafe-Block notwendig. Dieser deaktiviert viele Sicherheitsprüfungen und erlaubt dem Programmierer die Verwendung unsicherer Konstrukte wie rohe Zeiger und ungeprüfte Typkonvertierungen. Folglich (unter Annahme eines korrekten Compilers) sind undefiniertes Programmverhalten oder spontane Abstürze (Segfaults) immer auf einen unsafe-Block oder die daraus resultierenden Daten zurückzuführen, was die Fehlerkorrektur für den Programmierer erleichtert.

(Beispiel)

Jakt legt großen Wert auf ergonomische Fehlerbehandlung. Der normale Fehlertyp ErrorOr, wie er aus SerenityOS bekannt ist, kann mittels throws in Kombination mit einem Rückgabewert für den Regelfall bei einer Funktion angegeben werden. try prüft einen Ausdruck auf Fehler und gibt diesen falls nötig an den Aufrufer zurück, was dem ?-Operator in Rust ähnlich kommt. Der guard else-Ausdruck erlaubt das Prüfen von Vorbedingungen und das Angeben einer Abbruchstrategie, sollte eine Vorbedingung nicht erfüllt sein.

(Beispiel: Try, throws)

function demonstrate_guards() {
    mut i = 0
    loop {
        // Inkrementiert i nur, wenn es ungerade ist.
        guard i % 2 == 0 else {
            i++
            continue
        }
        // Bricht die Endlosschleife bei i = 10 ab.
        guard i < 10 else {
            break
        }

        println("{}", i++) // -> 0 2 4 8
    }
}

C++-Interoperabilität

Bearbeiten

Da Jakt mit einer substantiellen C++-Codebase zusammenarbeiten muss, ist die C++-Interoperabilität eine zentrale Funktion. Mit extern können C++-Klassen und Funktionen direkt in Jakt verwendet werden, was insbesondere wegen der identischen ABI möglich ist. Mittels cpp-Blöcken kann außerdem C++-Code direkt in Jakt eingefügt werden, um auf fehlende oder unsichere Funktionalität zurückzugreifen. Da C++ per Definition eine nicht speicher- oder typsichere Sprache ist, sind auch cpp-Blöcke unsicher.

(Beispiel: extern)

mut i: i32? = None
unsafe {
    cpp {
        "i = 32;"
    }
}

let x = 3
unsafe {
    cpp {
        "auto const y = 7;"
        "*i += (x + y);"
    }
}

println("{}", i!) // -> 42
  • Weitere generische Aspekte?
  • Funktionale Aspekte

Implementierung

Bearbeiten

Jakt ist primär eine vollständig kompilierte Programmiersprache ohne interpretierte Zwischenstufe. Stand September 2022 gibt es zwei offizielle Jakt-Implementierungen, von denen eine in Rust und eine in Jakt verfasst ist. Die Rust-Implementierung ist in der Zwischenzeit nicht mehr in der aktuellen Version des Quellrepositories zu finden, sie diente als Bootstrap für den sogenannten Selfhost-Compiler. Dieser in Jakt verfasste Compiler ist in der Lage, sich selbst zu übersetzen (self compiling bzw. self hosting) und wird daher für die weitere Entwicklung verwendet. Der Rust-Compiler unterstützt damit nicht die aktuellste Jakt-Variante und ist vermutlich weniger stabil.

Beide Compiler produzieren keinen Maschinencode oder Assemblerbefehle, sondern C++-Quellcode, der mit einem C++20-Compiler übersetzt werden kann; als Teil der normalen Toolchain wird nur Clang unterstützt. Daher sind sowohl Rust-Compiler als auch Selfhost-Compiler technisch gesehen keine Compiler, sondern Transpiler, da sie eine Programmiersprache in eine andere übersetzen. Das Jakt-Projekt hat jedoch explizit das Ziel, Jakt-Semantiken nicht von C++ abhängig zu machen und irgendwann direkt in Maschinencode oder eine andere niedrige Darstellung (Assembler, LLVM IR etc.) zu übersetzen.

Es existiert ein Jakt-Interpreter, der auf dem Backend des Selfhost-Compilers aufbaut. (LINK)

Mit dem Rust-Compiler gelang es via WebAssembly, Jakt direkt im Browser nach C++ zu transpilieren; ein z.B. von Matt Godbolts Compiler Explorer bekanntes Prinzip. (LINK)

Einzelnachweise

Bearbeiten
  1. Memory safety for SerenityOS. In: awesomekling.github.io. 19. Mai 2022, abgerufen am 18. September 2022.
  2. jakt/samples at main · SerenityOS/jakt. Abgerufen am 15. September 2022 (englisch).
  3. The Jakt programming language. SerenityOS, 15. September 2022, abgerufen am 15. September 2022.
  4. jakt/selfhost at main · SerenityOS/jakt. Abgerufen am 15. September 2022 (englisch).

Kategorie:Programmiersprache