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.


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:

ApplyRefinement {
    target:     TypeSubject
    refinement: TypeFnId
    args:       [Tag]
}

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)
}
:> Share(Local)    // erzeugt ApplyRefinement(target, 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) -> 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