Zum Inhalt

Jade / JDL — Design Entscheidungen

Session: 28. Februar 2026
Motto: "Get fucked, McCarthy." 🍪


.. Operator / Interval

  • .. ist ein normaler protokollgebundener Operator — kein Sonderfall im Compiler
  • Spannable via <{ operator: ".." }> — exakt wie Add, Sub, etc.
  • Zwei unabhängige Äste:
  • Steppable[T]Range[T] (diskret)
  • Interpolatable[T]Interval[T] (kontinuierlich)
  • Umbenennung: Span[T]Interval[T] — klarer, unbelastet von Systemprogrammierungs-Konnotationen
  • Driver-Konzept: Pipeline bleibt pure, Effekte kommen vom externen Driver
  • Ergebnis: vollständig reaktives System als emergente Eigenschaft — nicht als Feature
// Pure Pipeline
val gradient = Color.green..Color.red

// Driver bringt Effekt — Pipeline bleibt unverändert
gradient.driveWith(Clock.tick(16ms))      // Zeit
gradient.driveWith(scrollPosition())      // Scroll
gradient.driveWith(audioLevel())          // Audio

Parser Kombinator

  • Parser Kombinator — nicht Parser Generator
  • Combinators: once(), many(), optional(), sequence(), choice()
  • D-Attribute: @nogc nothrow pure wo möglich
  • struct statt class — kein GC-Overhead
  • ref statt Pointer in Parametern
  • sumtype für AST-Knoten — kein class-Vererbungsbaum
  • D-Slices als Zero-Copy Fenster in bestehende Buffer
  • JadeResult(T) als einheitlicher Ergebnistyp — drei Zustände: Ok(T), Err(Diagnostic), Empty
  • Empty ersetzt Option — kein separater Typ nötig
  • Diagnostic ist immer der Fehlertyp — kein generisches E
  • Kein Exception-Mechanismus
  • Vollständige Definition: siehe jade-result-spec.md
  • Granulares Testen auf jeder Kombinator-Ebene möglich

Diagnostics / Fehlermeldungen

Eigene Diagnostic-Struktur von Anfang an — jede Phase hat eigenes Fehlermodell:

struct Diagnostic {
    Phase      phase;      // Lexer, Parser, TypeEngine, ...
    Severity   severity;   // Error, Warning, Info
    SourceSpan span;       // Hauptstelle im Code
    string     message;    // Hauptmeldung
    Hint[]     hints;      // mit eigenem Span — zeigen auf andere Stellen
    Note[]     notes;      // ohne Span — erklärend
}

struct Hint {
    SourceSpan span;
    string     message;    // konstruktiv: "Füge X hinzu"
}

struct Note {
    string message;        // erklärend: "weil Y..."
}
  • Hints zeigen auf andere Stellen im Code als der Fehler selbst
  • LSP-Grundlage ist damit bereits vorhanden
  • Fehlermeldungen nutzen den reichen Typ-Kontext jeder Phase

Memory Policies — Umbenennung

RefRc (RefCounted) — vermeidet Verwechslung mit "Referenz"

"Rc bedeutet: der Wert gehört sich selbst — wird vernichtet wenn nichts mehr auf ihn zeigt."

Fünf primitive JME-Verhaltensweisen — alles andere ist Komposition:

Policy Semantik Wann
Value Stack-inline, kein Handle Kleine Primitives, POD
Rc RefCounted, free wenn refcount = 0 Dynamische Daten, Vec, str
Weak Kein Refcount, upgrade() → Option Parent-Kind-Beziehungen
Arena[A] Bump-Allokation, Bulk-Reset Request-Daten, temporäre Werte
Pool[P] Freelist, Wiederverwendung Verbindungen, teure Ressourcen

Arenen

  • ArenaScope-Protokoll nicht nötigwith ist ein Compiler-Konstrukt
  • Compiler generiert open/reset/close direkt aus dem Refinement
  • Arena-Typen brauchen nur ihr Refinement:
type RequestArena: struct {
    capacity: usize
    growable: bool
} :> <{ memory: Arena[RequestArena] }>

Was intern passiert:

open()   →  Speicherblock reservieren, cursor = base, auf Thread-Stack pushen
alloc()  →  cursor += size  (implizit, ~5ns)
reset()  →  cursor = base  (ein Schreibvorgang)
close()  →  reset + free + vom Stack poppen

Slices

  • Slice im selben Scope → View — ptr + len, kein Kopieren, zero-cost
  • Slice über Scope-Grenze → automatische Kopie in Ziel-Arena
  • Keine Lifetime-Annotationen — Ergonomie vor maximaler Performance
  • Generation-Counter als Laufzeit-Sicherheitsnetz
  • Wer Performance will → Generator[T] statt Slice zurückgeben
// Innerhalb des Scopes — View, zero-cost
val arr   = [1, 2, 3, 4, 5]
val slice = arr[1..3]          // ptr + len — kein Kopieren
process(slice)                 // arr lebt noch — OK

// Über Scope-Grenze — automatische Kopie
def firstHalf(arr: [i32]) -> [i32] {
    arr[0..arr.len / 2]        // Compiler kopiert automatisch
}

"Wer so faul ist und einen Slice zurückgibt statt eines Generators, muss halt das Kopiergeld zahlen."


Strings

  • UTF-8 by default
  • Immutable by default
  • Keine rohen Pointer in JDL — nur im FFI-Kontext Ptr[T]
  • Mutation → automatische Kopie in Ziel-Arena

Interning-Strategie:

Compile-Zeit:   alle String-Literale → Interning-Table im Binary
Runtime:        kurze Strings (< 32 Bytes) → optionales Runtime-Interning
                lange Strings → nie interned

StringBuilder via generisches Protokoll:

protocol Buildable[T] {
    def append(self, value: T) -> Self
    def build(self)            -> T
}

// Verwendung — Method Chaining mit "."
val s = str.builder()
    .append("Hello, ")
    .append(name)
    .append("!")
    .build()

Collections

Array[T] — dynamisch wachsend

Umbenennung: Vec[T]Array[T] — kein Rust-Smell

type Array[T]: struct {
    len: usize
    cap: usize
} :> <{ memory: Rc }>   // Heap — kann reallokieren
  • memory: Rc — Reallokation beim Wachsen braucht Heap-Freiheit
  • JME bewegt Speicher transparent — Handle bleibt gleich
  • withCapacity als Optimierung für bekannte Größen

Statische Arrays via typefn

typefn StackBuffer[T, N] where N: Literal[usize] =
    struct {
        data:   [T; N]
        filled: usize
    } :> <{ memory: Value }>

// Verwendung
type Rgb  = StackBuffer[u8, 3]
type Rgba = StackBuffer[u8, 4]
type Mat4 = StackBuffer[f32, 16]

Array-Familie — einheitliche Notation

[T]         →  Slice / View  (kein Ownership)
[T; N]      →  StackArray    (compile-time Größe, Value)
Array[T]    →  HeapArray     (dynamisch, Rc)

Map[K, V]

Via typefn — verschiedene Implementierungen, ein Protokoll:

typefn HashMap[K, V] where K: Hashable + Equatable =
    struct {
        buckets: Array[Option[(K, V)]]
        len:     usize
    } :> <{ memory: Rc }>

typefn LinkedHashMap[K, V] where K: Hashable + Equatable =
    HashMap[K, V] :> <{ ordered: true }>

typefn TreeMap[K, V] where K: Comparable =
    struct {
        root: Option[Rc[Node[K, V]]]
        len:  usize
    } :> <{ memory: Rc }>

typefn SmallMap[K, V, N] where K: Equatable, N: Literal[usize] =
    struct {
        entries: StackBuffer[(K, V), N]
        len:     usize
    } :> <{ memory: Value }>   // Stack-allocated!

// Der Default
typefn Map[K, V] where K: Hashable + Equatable =
    HashMap[K, V]

Einheitliches Protokoll:

protocol Collection[K, V] {
    def get(self, key: K)         -> Option[V]
    def set(self, key: K, val: V) -> Self
    def delete(self, key: K)      -> Self
    def contains(self, key: K)    -> bool
    def len(self)                 -> usize
}

Bäume / Hierarchien

  • Parent besitzt Kinder → Rc — refcount erhöht sich
  • Kind kennt Parent → Weak — kein refcount, nur Beobachter
  • Weak.upgrade()Option — sicherer Zugriff
  • Verhindert Referenzzyklen die Rc nie auf 0 bringen würden
type Node: struct {
    value:    i32
    parent:   Weak[Node]       // kennt — kein Refcount
    children: Array[Rc[Node]]  // besitzt — Refcount
}

Composability als Kernprinzip

JME implementiert 5 primitive Verhaltensweisen — alles andere ist Komposition in JDL.

JDL kann Typen beschreiben die D nicht ausdrücken kann:

type SensitiveData: struct {
    payload: [u8]
} :> <{
    memory: Arena[SecureArena]    // physisch verschlüsselter Speicher
    share:  Local                 // niemals zwischen Tasks
    derive: [!Copyable, !Inspectable]  // kann nicht kopiert oder geloggt werden
    create: Factory[SecureVault]  // nur SecureVault darf erstellen
    drop:   Custom                // explizites Zeroing beim Drop
}>
D:    "bitte nicht kopieren" — Kommentar
JDL:  derive: [!Copyable]   — Compile-Fehler wenn verletzt

D:    "nur im Request-Kontext verwenden" — Dokumentation
JDL:  memory: Arena[RequestArena]        — Compiler erzwingt es

D:    "nicht zwischen Threads teilen" — Hoffnung
JDL:  share: Local                    — VM erzwingt es

"Je mächtiger das Typsystem, desto weniger D brauche ich."


Stdlib-Features inspiriert von Effect-TS

Status: Informativ — Stdlib-Planung, noch nicht spec-reif.

Vier Konzepte aus Effect-TS die in der Jade-Stdlib landen sollen. Alle emergieren aus bestehenden Primitiven — kein neues VM-Primitiv, kein neues Sprachkonstrukt nötig.

Schedule[E] — komponierbare Retry-Policies

Ein Schedule[E] ist ein Closure-Typ-Alias:

// spekulativ
typefn Schedule[E] = (attempt: u32, error: E) -> Option[Duration]

def exponentialBackoff[E](base: Duration) -> Schedule[E] =
    (attempt, _) => Some(base * 2.pow(attempt))

def maxRetries[E](n: u32) -> Schedule[E] =
    (attempt, _) => if attempt < n then Some(0ms) else None

def both[E](a: Schedule[E], b: Schedule[E]) -> Schedule[E] =
    (attempt, error) => match (a(attempt, error), b(attempt, error)) {
        | (Some(d1), Some(d2)) => Some(d1.max(d2))
        | _                    => None
    }

// Verwendung:
val policy = exponentialBackoff(100ms)
    |> both(maxRetries(5))
    |> both(maxDelay(30s))

Kein neues Primitiv — Schedule[E] ist ein typefn Alias auf einen Closure-Typ.


RequestResolver — automatisches Batching

Löst das N+1 Problem. 50 Tasks die gleichzeitig getUserById aufrufen werden automatisch zu einer einzigen Query gebatcht.

Funktioniert über Scheduling: Tasks parken sich via vm_await, der Scheduler erkennt dass mehrere Tasks auf denselben Request-Typ warten, batcht sie, führt eine einzige Operation aus, resumed alle via vm_resume.

// spekulativ
protocol Batchable[Req, Res] {
    def batch(requests: [Req]) -> [Res]
}

Gehört in die Stdlib — ist ein Primitiv auf dem Frameworks aufbauen sollen.


Cause[E] — strukturierte Fehlerursachen

Result[T, E] verliert Fehler wenn zwei parallele Tasks gleichzeitig fehlschlagen. Cause[E] ist ein Fehler-Baum:

// spekulativ
type Cause[E]: enum =
    | Fail(E)
    | Interrupt
    | Sequential(Cause[E], Cause[E])
    | Parallel(Cause[E], Cause[E])

Der CallGraph-Generator nutzt Cause[E] automatisch für parallele Nodes — kein Fehler geht still verloren.

// spekulativ
CallGraph processOrder(input: OrderInput) -> Result[Order, Cause[OrderError]] {
    Node charge  { call: Payment.charge(input) }
    Node notify  { call: Email.send(input) }
    flow: [charge, notify]  // beide parallel — beide Fehler landen in Cause::Parallel
}

FiberRef[T] — Task-lokaler ambient State

Status: Deferred — kein konkreter Use Case bekannt.

Task-lokaler Storage der beim spawn automatisch in Child-Tasks kopiert wird. Klassischer Use Case: Trace-IDs die durch die gesamte Task-Hierarchie fließen ohne als Parameter durchgereicht zu werden.

Würde zwei neue VM-Intrinsics benötigen (vm_fiberref_get, vm_fiberref_set) und einen fiberRefs: Map[FiberRefId, Handle] Slot im TaskDescriptor.

Warum deferred: Die meisten Use Cases lassen sich eleganter über den Effect-Stil (service Tracing) oder Aktoren ausdrücken. Wird revisited wenn ein konkreter Use Case auftaucht der sich damit nicht lösen lässt. Die 19 Intrinsics bleiben vorerst stabil.


Scripting Host via .jdm + Hot-Reload

Status: Informativ — kein normativer Anspruch. Noch nicht vollständig durchdacht.

Jade benötigt keinen separaten Scripting-Layer (Lua, Python, etc.). Die VM selbst ist der Scripting Host — .jdm-Module sind die Scripts.

Grundprinzip: - Die Host-Applikation (z.B. Game Engine) exportiert ihre API als .jdm mit einem [Exports]-Abschnitt - Script-Code importiert diese .jdm — der Compiler prüft alles statisch - Scripts werden zu .jdm kompiliert und zur Laufzeit per vm_load_lib geladen - Scripts sind vollständig typgeprüft — Laufzeit-Typfehler wie in Lua entfallen

Hot-Reload via own: Shared: - Script-State lebt in Shared[ScriptState] — die Engine hält einen Handle - Beim Reload: alte Task stirbt → Refcount sinkt aber nicht auf 0 (Engine hält noch) - Neue Task startet mit neuer .jdm — bekommt denselben Shared[ScriptState] Handle - State überlebt den Reload ohne Sondermechanismus

jade build --module patrol.jdl   →  patrol.jdm
Engine erkennt neue patrol.jdm
→ alte PatrolTask graceful stoppen
→ neue PatrolTask mit neuer .jdm starten
→ Shared[ScriptState] Handle unverändert übergeben

Was der Script-User bekommt (im Vergleich zu Lua): - Volle statische Typprüfung - LSP-Support, Autocomplete, Hover-Typen - Compile-Fehler statt Laufzeit-Fehler - Dieselben Memory-Policies und Safety-Garantien wie normaler JDL-Code

Offene Fragen: - Wie granular kann Hot-Reload sein? (einzelne Funktion vs. ganzes Modul) - Wie wird die Script-API der Engine versioniert? - Sandboxing: Wie werden Script-Permissions eingeschränkt? (Ring 2 ist ein Anfang) - Workflow-Tooling: File-Watcher, automatisches Rebuild, Fehlerreporting im laufenden System


Offene Punkte für nächste Session

  • Shared Memory / share: Sync — Atomics, Mutex
  • Stack-Frame Layout
  • Closure Capture / Upvalues
  • FFI-Boundary — Buffer/Slices an C-Code
  • =? Error-Propagation intern in der VM

🍪🍪🍪 — Guter Tag für Jade.