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 ModusTrydie gewöhnliche Form->und wirft (jdl-sprachreferenz§12.2, §12.3). Ob->?als eigene Form für die Typ-Recovery ausDyn[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:
- 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. - Beim Dispatch (Trusted Core): Der Verifier prüft, dass die tatsächlich aufgerufene Implementierung die Festlegung einhält (
jdl-effect-rows-und-throwG-9). Ohne diese Prüfung wäre eineDyn-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 schreibtDyn[P].addendum-begriffe-und-motiveverlangt dagegen das Schlüsselwortdyn P, weilDyn[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-motivenenntDynValuedie einzige Wertekategorie mit Tag. Ein Union-Wert zur Laufzeit — etwa der gefangene Fehler einescatchmit dem TypA | 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 persistenteTypeId, und bleibt kleiner als einDynValue. 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-arbeitsnotizS-1), und Nutzercode erlangt keinen Handler als Wert (jdl-sprachreferenz§8.5, A-5). Eine Map ausDyn[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.