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.
1.1 TypeSubjects und Ring-0-Facts¶
Der Refinement-Mechanismus wirkt nicht auf „Structs“ im engen Sinn, sondern auf TypeSubjects: Primitive, Structs, Enums, Unions, Funktionen, Protocols, Blueprints und daraus abgeleitete Descriptor-Subjects. Ein TypeSubject besitzt einen Meta-Record, den TypeFns lesen, prüfen und erweitern können.
Davon strikt getrennt sind Ring-0-Facts. Ring-0-Facts sind axiomatische Eigenschaften, die von der VM, der Memory Engine oder dem Bootstrapper gesetzt werden. Beispiele sind Größe, Alignment, Registerklasse, primitive Repräsentation, Immediate-/Handle-Status und feste VM-Typklassen.
// Darstellung eines primitiven VM-Typs im Seed, nicht user-facing Syntax
type i32 : primitive {
size: 4
align: 4
memory: Value
class: SignedInteger
}
Refinements dürfen Ring-0-Facts lesen, aber nicht überschreiben. Sie erweitern oder beschränken ausschließlich den Meta-Record des Subjects.
i32 :> Derive([Inspectable]) // kann eine Protocol-Bindung erzeugen
i32 :> Memory(Ref) // Fehler: `memory: Value` ist Ring-0-Fact
Runtime-orchestrierende Refinements wie Retry, Timeout, ResourceLimits, Parallel oder OverrunPolicy sind nur auf Blueprint- oder Engine-Subjects zulässig. Speicher-, Besitz-, Share-, Drop-, Derive-, Operator- und FFI-Descriptor-Refinements bleiben auf den jeweils kompatiblen TypeSubjects zulässig. Der Operator :> bleibt einheitlich; die Zulässigkeit folgt aus den Prämissen der TypeFn.
1.2 ApplyRefinement und Materialisierung¶
Ein Suffix-Refinement wird intern nicht als normaler Funktionsaufruf behandelt, sondern als ApplyRefinement-Record:
Der :>-Operator liefert das target. Die Refinement-TypeFn wird danach in einem TypeFnContext evaluiert. db.target() zeigt auf dieses Subject. Die TypeFn erzeugt einen MetaPatch; erst der CompilerDB-Materializer committed diesen Patch nach Ring-, Coherence- und Konfliktprüfung in den Meta-Record.
Damit ist :> Share(Send) source-seitig kurz, aber intern vollständig explizit: Target, TypeFn, Argumente, Patch und Proof Trace sind nachvollziehbar.
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¶
Präfix-Notation und Suffix-Notation beschreiben semantisch denselben Meta-Record. Normativ wird die Suffix-Notation jedoch über ApplyRefinement und MaterializationPatches verarbeitet. Der Compiler materialisiert geprüfte Patches in den Meta-Record:
// 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. Für normalen user-facing Code ist :> die idiomatische Form, weil sie TypeFn-Prämissen, Diagnostics und Proof Trace sauber durchläuft:
- Präfix ist kanonische Darstellung für Specs, Diagnostics, Seeds und Profildefinitionen.
- Suffix ist die normale Refinement-Syntax im Quelltext.
3. Refinement-Konstruktoren¶
Ein Refinement-Konstruktor ist eine typefn, die auf ein TypeSubject angewendet wird und einen MetaPatch erzeugt. Er nimmt typisierte Argumente entgegen; das Ziel-Subject kommt bei :> aus dem ApplyRefinement-Kontext.
Definition und Aufruf¶
type SharePolicy: enum = | Local | Send | Sync
typefn Share(policy: SharePolicy) -> MetaPatch {
val target = db.target()
db.setMeta(target, "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) -> MetaPatch {
val target = db.target()
db.setMeta(target, "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) -> MetaPatch {
val target = db.target()
db.setMeta(target, "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) -> MetaPatch {
val target = db.target()
db.setMeta(target, "share", policy)
}
Initialisierungs-Policies¶
type CreatePolicy[F]: enum =
| Trivial // Struct-Literal überall erlaubt
| Factory[F] // nur F darf konstruieren
typefn Create(policy: CreatePolicy[F]) -> MetaPatch {
val target = db.target()
db.setMeta(target, "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) -> MetaPatch {
val target = db.target()
db.setMeta(target, "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]) -> MetaPatch {
val target = db.target()
db.setMeta(target, "derive", protocols)
}
// !Protocol im Array markiert explizite Nicht-Implementierung
// Operator-Bindung für Protokolle:
typefn Operator(symbol: str) -> MetaPatch {
val target = db.target()
db.setMeta(target, "operator", symbol)
}
// FFI-Symbol-Bindung:
typefn CSymbol(name: str) -> MetaPatch {
val target = db.target()
db.setMeta(target, "cSymbol", name)
}
// FFI-Library-Linkage für extern-Blöcke:
typefn FFILink(linkage: str) -> MetaPatch {
val target = db.target()
db.setMeta(target, "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) -> MetaPatch {
val target = db.target()
db.setMeta(target, "cached", policy)
}
// Verwendung — nicht unterscheidbar von eingebauten Refinements:
type UserProfile: struct { ... } :> Cached(Ttl(300))
Der Compiler evaluiert Cached(...) als Refinement-TypeFn, erzeugt einen MetaPatch und materialisiert ihn nach erfolgreicher Prüfung in den Meta-Record des Typs. Was damit passiert liegt beim jeweiligen Framework oder bei der jeweiligen Engine — 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 <{ }> — kanonische Darstellung / Seeds / Profile
Suffix :> Constructor(...) — normale Refinement-Syntax im Quelltext
:> liefert das Target und erzeugt ApplyRefinement
Materialisierung erfolgt über geprüfte Patches
Konstruktoren: typefn Name(arg: PolicyType) -> MetaPatch
`:>` erzeugt ApplyRefinement(target, Name, args)
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
TypeFn erzeugt Patch, Materializer committed, Engine/Framework wertet aus
Ableitung: Compiler leitet Policies automatisch ab
Explizit nur wenn Ableitung falsch oder Absicht dokumentiert
Spätere :> gewinnen bei Konflikten