Zum Inhalt

JDL Refinement- und Meta-System

Dieses Dokument legt die Grundlage für alle drei Teile der Sprachspezifikation. Es beschreibt das Meta-Record-System, die Refinement-Syntax, den Profil-Mechanismus und die Designphilosophie hinter JDLs starker Typisierung.


0. Designphilosophie — Konventionen statt Magie

JDL ist eine stark typisierte Sprache. Das bedeutet nicht: der Compiler zwingt den Entwickler zu endloser Explizitheit. Es bedeutet: dieselben wenigen Regeln gelten überall, auf jeder Ebene, ohne Ausnahme.

Rust löst Speicher- und Nebenläufigkeitsprobleme durch explizite Lifetime-Annotationen und den Borrow Checker — mächtig, aber der Entwickler verhandelt ständig mit dem Compiler. Python löst es durch Weglassen — bequem, aber ganze Klassen von Fehlern verschieben sich von der Compile-Zeit in die Laufzeit. JDL wählt einen dritten Weg:

Der Compiler kennt die fundamentalen Wahrheiten. Der Entwickler deklariert Absicht. Der Rest folgt automatisch.

Ein Typ der share: Local trägt, verlässt nie einen Task — nicht durch eine explizite Prüfung an jeder Verwendungsstelle, sondern weil der Compiler diese Invariante axiomatisch behandelt. Ein Typ der drop: Custom trägt, wird deterministisch aufgeräumt — immer, auch bei Early-Return, auch bei Fehler, ohne dass der Entwickler daran denken muss.

Diese Regeln sind keine Konventionen die man kennen und befolgen muss. Sie sind Teil des Typsystems und damit untrennbar Teil der Sprache selbst. JDL mag auf den ersten Blick überwältigend wirken — die Konzepte sind ungewohnt. Aber sobald man erkennt dass alles einer einzigen intrinsischen Logik folgt, dreht sich das Bild: JDL hält nicht zurück. JDL hält den Rücken frei.

Das Refinement-System ist das Vehikel dieser Philosophie. Statt Magic-Traits, Marker-Interfaces oder Compiler-Pragmas gibt es in JDL einen einzigen, einheitlichen Mechanismus: Meta-Records die Policies als Daten ausdrücken, und Refinement-Konstruktoren die diese Policies typsicher setzen. Wer diesen Mechanismus versteht, versteht wie der Compiler denkt.


1. Meta-Records — anonyme Typ-Ebene

<{ ... }> ist ein anonymer struktureller Typ auf Meta-Ebene — das direkte Pendant zum anonymen Wert-Record:

// Anonymer Wert-Record:
val point = { x: 1.0, y: 2.0 }

// Anonymer Meta-Record:
type <{ share: Local, own: Unique }> DbConnection: struct { handle: HandleId }

Beides sind namenlose, strukturelle Records. Der Unterschied ist die Ebene: Wert-Records existieren zur Laufzeit, Meta-Records existieren ausschließlich zur Compile-Zeit. Der Compiler liest das Meta-Record einer Definition und leitet daraus Allokationsstrategie, Ownership- Semantik, zulässige Task-Grenzen sowie Konstruktions- und Destruktionsverhalten ab.

Meta-Records sind keine Annotationen — sie sind Daten. Das hat eine wichtige Konsequenz: sie können wie jeder andere Typ benannt, wiederverwendet und kombiniert werden. Darauf baut das Profil-System in Abschnitt 4 auf.


2. Zwei äquivalente Schreibweisen

JDL kennt zwei syntaktisch gleichwertige Formen um Meta-Records an Typen, Funktionen und Protokollen zu setzen.

Präfix — <{ }>

Das Meta-Record steht direkt nach dem Keyword, vor dem Namen:

type <{ share: Local, own: Unique }> DbConnection: struct { handle: HandleId }

protocol <{ operator: "+" }> Add {
    def add(self, other: Self) -> Self
}

Suffix — :>

Refinement-Konstruktoren folgen nach dem Body der Definition. Mehrere werden gekettet:

type DbConnection: struct {
    handle: HandleId
} :> Share(Local) :> Own(Unique)

protocol Add {
    def add(self, other: Self) -> Self
} :> Operator("+")

Äquivalenz

Beide Formen sind vollständig äquivalent. Der Compiler merged :>- Konstruktoren intern in das Meta-Record der Definition:

// Identisch:
type <{ share: Local, own: Unique }> DbConnection: struct { handle: HandleId }
type DbConnection: struct { handle: HandleId } :> Share(Local) :> Own(Unique)

Die Wahl ist eine Frage der Lesbarkeit — keine Form ist bevorzugt:

  • Präfix passt wenn die Policies zur Signatur gehören und zuerst sichtbar sein sollen — typisch bei kurzen Typen.
  • Suffix passt wenn die Definition im Vordergrund steht und Policies nachrangig sind — typisch bei längeren Structs und Protokollen.

3. Refinement-Konstruktoren

Ein Refinement-Konstruktor ist eine typefn die ein Meta-Record- Fragment produziert. Er nimmt einen typisierten Wert entgegen und expandiert zur Compile-Zeit zum entsprechenden Meta-Record-Eintrag.

Definition und Aufruf

type SharePolicy: enum = | Local | Send | Sync

typefn Share(policy: SharePolicy) = <{ share: policy }>
:> Share(Local)    // expandiert zu: <{ share: Local }>

Das Argument ist durch SharePolicy beschränkt — der Compiler prüft dies statisch:

:> Share(Local)    // ok
:> Share(Unique)   // COMPILERFEHLER: Unique ist kein SharePolicy

Listen als Argumente

Konstruktoren die mehrere Werte entgegennehmen erhalten immer eine Liste — nie mehrere positionelle Argumente. Das hält die Aufruf- Syntax einheitlich unabhängig davon ob ein oder zehn Werte übergeben werden:

:> Derive([Equatable, Hashable])   // Protokolle

4. Profile — benannte Meta-Records

Da <{ ... }> ein Typ ist, kann er einen Namen erhalten:

type TaskLocal    = <{ own: Unique, share: Local }>
type ThreadSafe   = <{ own: Shared, share: Sync }>
type Transferable = <{ own: Unique, share: Send }>

Ein solcher Typ-Alias auf ein Meta-Record ist ein Profil — ein wiederverwendbares Bündel von Policies. Kein eigenes Keyword, kein Sondermechanismus. Wer type und <{ }> versteht, versteht Profile.

Verwendung

type UserManagerState: struct {
    users:  Map[str, User]
    nextId: i64
    stats:  UserStats
} :> TaskLocal

type SharedConfig: struct {
    host:  str
    port:  u16
    dbUrl: str
} :> ThreadSafe

Überschreibung

Spätere Refinements gewinnen bei Konflikten. Ein Profil setzt Defaults — einzelne Policies können danach gezielt überschrieben werden:

type TaskLocal = <{ own: Unique, share: Local, drop: Trivial }>

// share: Local und drop: Trivial werden überschrieben:
type DbConnection: struct { handle: HandleId }
    :> TaskLocal
    :> Share(Send)
    :> Drop(Custom)
// Resultat: <{ own: Unique, share: Send, drop: Custom }>

Stdlib-Profile

Die Standardbibliothek liefert Profile für häufige Kombinationen:

type TaskLocal    = <{ own: Unique, share: Local }>
type ThreadSafe   = <{ own: Shared,  share: Sync }>
type Transferable = <{ own: Unique, share: Send }>
type ValueType    = <{ memory: Value, drop: Trivial }>
type ManagedRef   = <{ memory: Ref,   own: Shared }>

Ein Profil das in einer Bibliothek definiert wurde verhält sich identisch zu einem das der Entwickler selbst geschrieben hat — es gibt keine privilegierten Profile.


5. Compiler-interne Refinements

Diese Refinements sind im Compiler hard-verdrahtet, weil sie die Code-Generierung direkt beeinflussen — Speicher-Layout, Ownership- Semantik, Initialisierung und Zerstörung. Der Compiler muss sie erkennen ohne erst eine typefn-Definition zu lesen.

Speicher-Policies

type MemoryPolicy: enum =
    | Value       // Stack-inline, kein Handle
    | Ref         // Heap, Reference-Counted, Handle
    | Arena[A]    // Bump-Allokation in Arena A
    | Pool[P]     // Wiederverwendung aus Pool P

typefn Memory(policy: MemoryPolicy) = <{ memory: policy }>
type UserStats: struct { ... }    :> Memory(Value)
type DbConnection: struct { ... } :> Memory(Pool[ConnectionPool])

Ownership-Policies

type OwnPolicy: enum =
    | Unique    // genau ein logischer Besitzer — Move-Semantik
    | Shared    // mehrere Besitzer, Reference-Counted (Arc-Semantik)
    | Weak      // nicht-besitzende Referenz auf Shared — kein Refcount,
                // Zugriff nur via upgrade() -> Option[Shared]

typefn Own(policy: OwnPolicy) = <{ own: policy }>

Weak löst das Referenzzyklen-Problem das bei Shared (Arc-Semantik) entstehen kann: zwei Werte die sich gegenseitig Shared referenzieren werden nie aufgeräumt weil der Refcount nie null erreicht. Weak hält keinen Refcount — der referenzierte Wert kann dropped werden. Zugriff erfolgt nur über upgrade() das None zurückgibt wenn der Wert bereits dropped ist:

val strong: Shared[Node] = Node { value: 42, next: None }
val weak: Weak[Node]     = strong.downgrade()

// später:
match weak.upgrade() {
    | Some(node) => Console.print(f"value: {node.value}")
    | None       => Console.print("Node wurde bereits aufgeräumt")
}

Typische Anwendungsfälle: Parent-Child-Strukturen (Child hält Weak auf Parent), Observer-Pattern (Observer hält Weak auf Subject), Cache-Einträge die den gecachten Wert nicht am Leben halten sollen.

Share-Policies

type SharePolicy: enum =
    | Local    // bleibt im erzeugenden Task — niemals crossTask
    | Send     // kann in anderen Task moved werden
    | Sync     // gleichzeitig aus mehreren Tasks nutzbar

typefn Share(policy: SharePolicy) = <{ share: policy }>

Initialisierungs-Policies

type CreatePolicy[F]: enum =
    | Trivial      // Struct-Literal überall erlaubt
    | Factory[F]   // nur F darf konstruieren

typefn Create(policy: CreatePolicy[F]) = <{ create: policy }>

Destruktions-Policies

type DropPolicy: enum =
    | Trivial    // kein Drop-Code nötig (Value-Types, POD)
    | Custom     // provide Disposable muss implementiert sein

typefn Drop(policy: DropPolicy) = <{ drop: policy }>

6. Erweiterbare Refinements

Diese Refinements sind über typefn implementiert und werden vom Compiler nicht speziell behandelt — er merged sie ins Meta-Record. Bibliotheken, Makros und Tools werten sie aus.

Eingebaute erweiterbare Refinements

// Auto-Derive von Protokollen:
typefn Derive(protocols: [protocol | !protocol]) = <{ derive: protocols }>
// !Protocol im Array markiert explizite Nicht-Implementierung

// Operator-Bindung für Protokolle:
typefn Operator(symbol: str) = <{ operator: symbol }>

// FFI-Symbol-Bindung:
typefn CSymbol(name: str) = <{ cSymbol: name }>

// FFI-Library-Linkage für extern-Blöcke:
typefn FFILink(linkage: str) = <{ link: linkage }>

User-definierte Refinements

Jeder kann eigene Refinement-Konstruktoren definieren — der Mechanismus ist identisch zu den eingebauten:

type CachePolicy: enum = | NoCache | Ttl(seconds: u32) | Permanent

typefn Cached(policy: CachePolicy) = <{ cached: policy }>

// Verwendung — nicht unterscheidbar von eingebauten Refinements:
type UserProfile: struct { ... } :> Cached(Ttl(300))

Der Compiler merged <{ cached: Ttl(300) }> ins Meta-Record des Typs. Was damit passiert liegt beim jeweiligen Framework oder Makro-System — JDL macht keine Annahmen über unbekannte Meta-Record-Einträge.


7. Policy-Ableitung

Im Normalfall leitet der Compiler Policies automatisch ab. Explizite Refinements sind nur nötig wenn die abgeleitete Policy nicht korrekt ist oder die Absicht explizit dokumentiert werden soll.

// Compiler leitet ab:
type UserStats: struct { totalUsers: i64, activeUsers: i64, avgAge: f64 }
// → Memory(Value): 24 Bytes, nur Primitive, kein Handle

type User: struct { name: str, email: str, age: i32 }
// → Memory(Ref): enthält str (Handle-Typ)

type DbConnection: struct { handle: HandleId }
// → Memory(Ref), Own(Unique): HandleId impliziert Unique-Ownership

Explizit überschreiben:

// Compiler würde Ref ableiten — wir erzwingen Value + Sync:
type UserStats: struct { ... } :> Memory(Value) :> Share(Sync)

// Oder via Profil:
type UserStats: struct { ... } :> ValueType :> Share(Sync)

Die Faustregel: Schreibe Refinements nur wenn der Compiler falsch läge oder wenn Absicht explizit dokumentiert werden soll. In allen anderen Fällen arbeitet die Ableitung korrekt — und der Code bleibt schlank.


8. Vollständige Policy-Übersicht

Refinement Argument Compiler-intern Beschreibung
Memory(p) MemoryPolicy ja Allokationsstrategie
Own(p) OwnPolicy ja Ownership-Semantik
Share(p) SharePolicy ja Task-Crossing-Regeln
Create(p) CreatePolicy[F] ja Konstruktions-Kontrolle
Drop(p) DropPolicy ja Destruktions-Logik
Derive([...]) [protocol\|!protocol] nein Auto-Implementierung
Operator(s) str nein Operator-Symbol-Bindung
CSymbol(s) str nein FFI C-Symbol-Name
FFILink(s) str nein Library-Linkage für extern-Blöcke

Zusammenfassung

Meta-Records:    <{ key: value }> — anonymer struktureller Typ, Compile-Zeit-Ebene
                 Pendant zum anonymen Wert-Record auf Laufzeit-Ebene
                 Kein Laufzeit-Overhead — existiert nur für den Compiler

Syntax:          Präfix <{ }> — nach Keyword, vor Name
                 Suffix :> Constructor(...) — nach Definition, gekettet
                 Beide Formen vollständig äquivalent
                 Wahl nach Lesbarkeit — keine Konvention erzwungen

Konstruktoren:   typefn Name(arg: PolicyType) = <{ key: arg }>
                 Argumente typsicher durch Union-Typen beschränkt
                 Mehrere Werte immer als Liste: Derive([A, B, C])

Profile:         type MyProfile = <{ key: value, ... }>
                 Benannter Typ-Alias auf Meta-Record
                 Wiederverwendbar, kombinierbar, überschreibbar
                 Kein eigenes Keyword — gewöhnlicher Typ-Alias
                 Spätere :> überschreiben frühere bei Konflikten

Compiler-intern: Memory, Own (Unique | Shared | Weak), Share, Create, Drop
                 Hard-verdrahtet — beeinflussen Code-Generierung direkt
                 Typsicher durch Policy-Unions beschränkt
                 Weak: nicht-besitzend, kein Refcount, upgrade() -> Option[T]

Erweiterbar:     Derive, Operator, CSymbol, FFILink — über typefn implementiert
                 User-definierbar mit identischem Mechanismus
                 Compiler merged, Framework/Makro wertet aus

Ableitung:       Compiler leitet Policies automatisch ab
                 Explizit nur wenn Ableitung falsch oder Absicht dokumentiert
                 Spätere :> gewinnen bei Konflikten