Zum Inhalt

Dyn[P] — Dynamische Protocol-Werte

Status: Normativ (v0.2.1)
Geltungsbereich: Typsystem, VM-Dispatch, Runtime-Repräsentation
Abhängigkeiten: 02-typsystem.md, dispatch-modes.md, 11-jadevalue-und-register-layout.md, 10-runtime-handles-und-memory-safety.md, jdl-effect-rows-und-throw.md §4.5
Autor: Elias


1. Zweck

Dyn[P] ist JDL's Mechanismus für dynamischen Protocol-Dispatch. Er löscht den konkreten Typ eines Wertes und ersetzt ihn durch ein Protocol-Interface. Der konkrete Typ ist zur Compile-Zeit nicht bekannt, das Protocol schon.

Dyn[P] ist semantisch äquivalent zu Rust's dyn Trait: immer Protocol-gebunden, niemals ungebunden. Es gibt in JDL keinen vollständig gelöschten "ich weiß nichts"-Typ. Jeder dynamische Wert hat mindestens eine Protocol-Garantie.


2. Dyn[P] ist kein TypeKind

TypeKinds (Primitive, Struct, Enum, Union, Function, Tuple, Blueprint, Flags) beschreiben welche Art von Typ etwas ist. Dyn[P] beschreibt dass der konkrete Typ gelöscht wurde.

Ein Dyn[Serializable] kann ein Struct sein, ein Enum, ein Blueprint — der TypeKind dahinter ist unbekannt. Dyn[P] ist orthogonal zu TypeKinds, nicht parallel. Er ist ein parametrischer Typ-Konstruktor der jeden TypeKind hinter einem Protocol-Interface wrappen kann.

Konsequenz: Dyn steht nicht in der TypeKind-Liste. Die VM kennt Dyn[P] als eigene Wertekategorie mit spezieller Repräsentation (siehe §5), nicht als TypeKind.

Seit 2026-10-05 ist auch row ein TypeKind (jdl-effect-rows-und-throw §3.6). Ein Dyn[P] kann keine Row wrappen, weil Rows keine Werte haben [A].


3. Syntax

// Deklaration eines dynamischen Protocol-Werts
val item: Dyn[Printable] = someValue

// In Funktionssignaturen
def process(items: [Dyn[Serializable]]) ->{Throw[ProcessError]} Unit

// Mehrere Protocol-Bounds (Intersection)
val entry: Dyn[Serializable + Printable] = someValue

P muss ein Protocol sein. Primitive Typen, Structs und andere nicht-Protocol Typausdrücke sind als Bound nicht zulässig.


4. Semantik

4.1 Protocol-Dispatch

Ein Dyn[P]-Wert garantiert, dass der verborgene Typ das Protocol P implementiert. Methoden von P können direkt aufgerufen werden:

def printAll(items: [Dyn[Printable]]) -> Unit {
    for item in items {
        item.print()  // dynamischer Dispatch — P ist bekannt, T nicht
    }
}

4.2 Typ-Recovery via Match

Der konkrete Typ kann zur Laufzeit über Pattern Matching wiederhergestellt werden:

def handleBlueprint(spec: Dyn[Blueprint]) ->{Throw[DispatchError]} Unit =
    match spec {
        | s: CallGraph     => run(s, effectRuntime)
        | s: ActorSpec     => run(s, actorSupervisor)
        | s: SchedulerSpec => run(s, scheduler)
        | _ => throw DispatchError.UnknownBlueprintType { received: spec.typeId }
    }

Innerhalb eines Match-Arms ist der Wert vollständig statisch typisiert. Das Match ist die Grenze zwischen dynamischer und statischer Welt.

Veraltet: Laufzeit-Matching auf Blueprint-Typen ist nicht mehr möglich, weil Blueprints nur in der Meta-Phase existieren (jdl-blueprint-spec §10.3). Das Beispiel zeigt die Form des Typ-Arms, nicht einen zulässigen Anwendungsfall. Für Union-Werte ist eine verwandte Form vorgeschlagen (jdl-sprachreferenz §16.3).

4.3 Typ-Recovery via TryCast

Außerhalb von Match kann der konkrete Typ über einen TryCast wiederhergestellt werden:

val concrete = try spec ->? CallGraph
// ab hier: concrete ist CallGraph, vollständig statisch typisiert

Der TryCast wirft CastError, die Row trägt Throw[CastError]. try markiert die Stelle; ohne catch wird der Fehler weitergereicht (jdl-effect-rows-und-throw §5.3).

[O] Seit der Effect Row hat jeder Cast im Modus Try die gewöhnliche Form -> und wirft (jdl-sprachreferenz §12.2, §12.3). Ob ->? als eigene Form für die Typ-Recovery aus Dyn[P] bestehen bleibt, ist nicht entschieden.

4.4 Erzeugung von Dyn[P]-Werten

Ein konkreter Wert wird zu einem Dyn[P]-Wert, wenn er an eine Stelle fließt die Dyn[P] erwartet — vorausgesetzt sein Typ implementiert P:

val user = User { name: "Alice", email: "alice@example.com" }

// User implementiert Printable → implizite Konversion zu Dyn[Printable]
val printable: Dyn[Printable] = user

// User implementiert Printable nicht? → Compile-Fehler

Die Konversion von T zu Dyn[P] ist implizit wenn T das Protocol P implementiert. Dies ist der einzige Punkt an dem der Compiler den konkreten Typ löscht und einen DynValue erzeugt.

4.5 Row-Plätze an einer Dyn-Stelle [V] (2026-10-05)

Ein Protocol kann einen Row-Platz ausweisen: eine Stelle, an der jede Implementierung eigene Services mitbringen darf, etwa type Needs : row::Row (jdl-effect-rows-und-throw §4.5). Weil DynValue ein Laufzeitwert ist, kommen an einer Dyn-Stelle nur Laufzeit-Plätze (row::Row) in Betracht; Meta-Rows erreichen die Laufzeit nie (jdl-effect-rows-und-throw §3.7). Bei einem konkreten Typ setzt der Compiler dort die echten Services der Implementierung ein.

An einer Dyn[P]-Stelle geht das nicht. Der konkrete Typ ist gelöscht, also auch der Inhalt des Platzes. Der Aufrufer muss aber vor dem Aufruf wissen, was er anfasst; sonst gäbe es Autorität, die in keiner Signatur steht.

Regel: Eine Dyn[P]-Stelle eines Protocols mit Row-Platz legt den Inhalt des Platzes fest, zum Beispiel „Dyn[Scopeable] mit Needs = {Fs}“. Die Schreibweise ist offen.

Daraus folgen zwei Prüfungen:

  1. Beim Erzeugen (§4.4): Ein Wert darf nur zu diesem Dyn[P] werden, wenn seine Implementierung nicht mehr verlangt, als die Stelle festlegt. Weniger ist erlaubt; das ist die Abschwächungsregel der Row.
  2. Beim Dispatch (Trusted Core): Der Verifier prüft, dass die tatsächlich aufgerufene Implementierung die Festlegung einhält (jdl-effect-rows-und-throw G-9). Ohne diese Prüfung wäre eine Dyn-Stelle ein Weg zu Autorität an der Row vorbei.

Ein Protocol ohne Row-Platz braucht keine Festlegung; seine Methoden dürfen in keiner Implementierung eigene Services mitbringen.

[O] Dieses Dokument schreibt Dyn[P]. addendum-begriffe-und-motive verlangt dagegen das Schlüsselwort dyn P, weil Dyn[P] wie ein gewöhnlicher generischer Typ aussieht. Die Regel hier gilt unabhängig davon; die Schreibweise der Festlegung hängt an der Entscheidung.


5. Runtime-Repräsentation

5.1 DynValue

Ein Dyn[P]-Wert wird zur Laufzeit als DynValue repräsentiert — ein JadeValue-Wrapper mit persistenter Typidentität:

// D-Implementierung (normativ für Struktur, nicht für Syntax)
struct DynValue {
    JadeValue payload;  // der verborgene Wert — Immediate oder Handle
    TypeId    typeId;   // persistente Runtime-Typidentität
}

DynValue ist größer als ein einzelner JadeValue-Slot (> 8 Byte) und wird vom JME auf dem Heap alloziert. An Funktionsgrenzen reist ein Dyn[P]-Wert als normales JadeValue.handle — ein HandleId der auf den heap-allozierten DynValue zeigt.

5.2 Heap-Allokation

Jeder Dyn[P]-Wert kostet eine JME-Allokation — auch wenn der verborgene Payload ein i32 ist. Das ist für die legitimen Anwendungsfälle von Dyn[P] (Plugin-Grenzen, Service-Registries, heterogene Collections, Netzwerk-Deserialisierung) akzeptabel, da diese typischerweise bei Initialisierung stattfinden, nicht in engen Loops.

5.3 Binding-Auflösung

Die interne Repräsentation der Protocol-Binding-Auflösung — ob Lookup-on-use, captured Witness, Protocol Table oder Strategy Slot pro (T, P) — ist VM-intern und nicht Teil des Sprachvertrags. Normativ ist nur: korrekte Binding-Auflösung zum Aufrufzeitpunkt muss garantiert sein.

Hat das Protocol einen Row-Platz, gehört zur Binding-Auflösung auch das Bündel der Services, das die Implementierung braucht. Seine Platzordnung ist die kanonische Ordnung der Row (jdl-effect-rows-und-throw G-8) [A].

5.4 Abgrenzung zu JadeValue

JadeValue ist ein ungetaggter Transportcontainer (Invariante: kein Laufzeit-Tag). Dyn[P] erfordert zwingend Runtime-Typinformation — der fundamentale Widerspruch zu JadeValue's No-Tag-Invariante. Dyn[P]-Werte sind daher eine eigene VM-interne Kategorie, die semantisch oberhalb von JadeValue liegt.

Ebene Beschreibung Tag?
Statischer / generischer Wert VM-interner Registerwert, kontextgetypt nein
JadeValue Grenzformat für Call/Return/Intrinsic/FFI nein (Bytecode-Kontext)
DynValue (Dyn[P]) Existential Container mit Runtime-Typinfo ja (erforderlich)

[O] Union-Werte. addendum-begriffe-und-motive nennt DynValue die einzige Wertekategorie mit Tag. Ein Union-Wert zur Laufzeit — etwa der gefangene Fehler eines catch mit dem Typ A | B | C (jdl-effect-rows-und-throw §7.2) — muss aber ebenfalls wissen, welches Mitglied er gerade ist, genau wie ein Enum seine Variantennummer. Vorgeschlagen ist, dass die VM eine Union intern wie ein Enum behandelt, dessen Varianten fremde Typen tragen (jdl-sprachreferenz §16.3). Dann trägt ein Union-Wert eine Variantennummer, keine persistente TypeId, und bleibt kleiner als ein DynValue. Entschieden ist das nicht; die Aussage im Addendum muss in jedem Fall nachziehen.


6. Anwendungsfälle

6.1 Heterogene Collections

val handlers: [Dyn[Handler]] = [
    userHandler,      // UserHandler
    orderHandler,     // OrderHandler
    paymentHandler    // PaymentHandler
]

for handler in handlers {
    handler.handle(request)  // dynamischer Dispatch auf Handler-Protocol
}

6.2 Plugin-Grenzen

def loadPlugin(path: str) ->{Throw[PluginError]} Dyn[EngineFactory] {
    val plugin = try Plugin.load(path)
    ret try plugin.resolve("engineFactory") ->? EngineFactory
}

Der Host kennt das EngineFactory-Protocol, nicht den konkreten Plugin-Typ.

6.3 Netzwerk-Deserialisierung

def receiveBlueprint(stream: ByteStream) ->{Throw[DeserializeError]} Dyn[Blueprint] {
    val typeTag = try stream.readTypeTag()
    val payload = try stream.readPayload()
    ret try Deserializable.deserialize(typeTag, payload)
}

Der deserialisierte Wert ist Dyn[Blueprint] — ein Blueprint dessen konkreter Typ erst zur Laufzeit via Match bestimmt wird.

Veraltet: Wie §4.2. Blueprints existieren nur in der Meta-Phase; über das Netz reist der Excerpt eines Descriptors, ein Record (00-ontology-types-tags-phantoms §3a.7). Das Beispiel zeigt nur noch die Form.

6.4 Service-Registries

type ServiceRegistry : struct {
    services: Map[str, Dyn[Service]]
}

def resolve[S: Service](registry: ServiceRegistry, name: str) ->{Throw[ServiceError]} S {
    val entry = registry.services.get(name) ?? throw ServiceError.NotFound { name }
    ret try entry ->? S
}

[O] Widerspruch. Ein Service hat keine Werte (jade-service-typekind-arbeitsnotiz S-1), und Nutzercode erlangt keinen Handler als Wert (jdl-sprachreferenz §8.5, A-5). Eine Map aus Dyn[Service] widerspricht beidem. Das Beispiel stammt aus der Zeit vor der Effect Row.


7. Nicht-Anwendungsfälle

Dyn[P] ist kein Ersatz für Generics. Wenn der konkrete Typ zur Compile-Zeit bekannt sein kann, ist statischer oder generischer Dispatch die richtige Wahl:

// Falsch — Dyn[P] ohne Grund
def addPair(a: Dyn[Addable], b: Dyn[Addable]) -> Dyn[Addable]

// Richtig — generisch, statisch auflösbar
def addPair[T: Addable](a: T, b: T) -> T

Dyn[P] ist ein gezieltes Werkzeug für Grenzen an denen der konkrete Typ nicht bekannt sein kann, nicht ein allgemeiner Abstraktionsmechanismus.


8. Normative Regeln

DYN-1:  Dyn[P] ist ein parametrischer Typ-Konstruktor, kein TypeKind.
        Er ist orthogonal zu TypeKinds und kann jeden TypeKind wrappen.
DYN-2:  P muss ein Protocol sein. Dyn ohne Protocol-Bound existiert nicht.
DYN-3:  Methodenaufrufe auf Dyn[P] dispatchen dynamisch über das
        Protocol P. Der konkrete Typ wird zur Laufzeit aufgelöst.
DYN-4:  Typ-Recovery erfolgt über Pattern Matching oder TryCast (->?).
        Innerhalb eines Match-Arms ist der Wert vollständig statisch
        typisiert.
DYN-5:  Die Konversion von T zu Dyn[P] ist implizit wenn T das
        Protocol P implementiert. Dies ist der einzige Punkt an dem
        der Compiler den konkreten Typ löscht.
DYN-6:  Ein Dyn[P]-Wert wird zur Laufzeit als DynValue repräsentiert —
        ein JadeValue-Wrapper mit persistenter TypeId. DynValue wird
        vom JME auf dem Heap alloziert.
DYN-7:  Dyn[P] ist kein JadeValue. DynValue liegt semantisch oberhalb
        von JadeValue und trägt zwingend Runtime-Typinformation.
DYN-8:  Dyn[P] ist kein Ersatz für Generics. Wenn der konkrete Typ
        zur Compile-Zeit bekannt sein kann, ist statischer oder
        generischer Dispatch die richtige Wahl.
DYN-9:  Hat P einen Row-Platz, legt die Dyn[P]-Stelle dessen Inhalt
        fest. Die Konversion nach DYN-5 ist nur zulässig, wenn die
        Implementierung nicht mehr verlangt; der Verifier prüft das
        beim Dispatch.                                   [V] (2026-10-05)

9. Merksatz

Dyn[P] heißt: ich kenne das Interface, nicht den Typ.
Ohne Protocol kein Dyn. Ohne Match kein Weg zurück.
Ohne festgelegten Row-Platz kein Dyn über ein Protocol, das einen hat.

Änderungsprotokoll

v0.2.1 (2026-10-05) — §4.5: Row-Platz mit Rolle row::Row; an einer Dyn-Stelle kommen nur Laufzeit-Plätze in Betracht. Syntax nachgezogen: Unit statt (), Throw in der Row statt Result-Rückgabe, try statt =?, ret im Blockrumpf (§3, §4.1 bis §4.3, §6.2 bis §6.4). §4.3: offen, ob ->? neben dem Try-Cast -> bleibt. §6.3 als veraltet markiert, §6.4 Widerspruch zu S-1 und A-5 benannt.

v0.2 (2026-10-05) — §4.5 neu: Row-Plätze an einer Dyn-Stelle müssen festgelegt werden, zwei Prüfungen, Trusted Core beim Dispatch [V]; Schreibweisen-Widerspruch zum Addendum benannt. DYN-9 neu. §2: row als TypeKind, nicht wrappbar. §4.2: Beispiel mit Dyn[Blueprint] als veraltet markiert. §5.3: Bündel des Row-Platzes. §5.4: offener Punkt zu Union-Werten und Tags. Merksatz ergänzt.

v0.1 — Initiale Fassung. Dyn[P] als parametrischer Typ-Konstruktor definiert, aus der TypeKind-Liste entfernt. Syntax, Semantik, Runtime-Repräsentation und Match-basierte Typ-Recovery spezifiziert. Abgrenzung zu JadeValue und Generics.