Jade / JDL — Design Entscheidungen¶
Session: 28. Februar 2026
Motto: "Get fucked, McCarthy." 🍪
.. Operator / Interval¶
..ist ein normaler protokollgebundener Operator — kein Sonderfall im CompilerSpannablevia<{ operator: ".." }>— exakt wieAdd,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 purewo möglich structstattclass— kein GC-Overheadrefstatt Pointer in Parameternsumtypefü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),EmptyEmptyersetztOption— kein separater Typ nötigDiagnosticist immer der Fehlertyp — kein generischesE- 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¶
Ref → Rc (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ötig —withist ein Compiler-Konstrukt- Compiler generiert
open/reset/closedirekt aus dem Refinement - Arena-Typen brauchen nur ihr Refinement:
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
memory: Rc— Reallokation beim Wachsen braucht Heap-Freiheit- JME bewegt Speicher transparent — Handle bleibt gleich
withCapacityals 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.
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.