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 }>
Das Argument ist durch SharePolicy beschränkt — der Compiler prüft
dies statisch:
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:
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