06 — JME, Handles, Tags und Phantom-Policies¶
Status: Normativer Konsolidierungsentwurf
Stand 2026-09-23: Abschnitt 12 (nativer Handle-Zustand) ist neu und auf jdl-vm-scheduler-spec v0.3.0 abgestimmt. Die Abschnitte 1 bis 11 sind dabei nicht überarbeitet worden; sie enthalten noch Bezüge auf D, typefn und die alte MemoryKind-Liste, die durch das Storage-Modell überholt ist.
1. Grundsatz¶
JDL kennt keine rohen Pointer in der Sprache.
JDL arbeitet mit:
Low-Level in JDL bedeutet nicht Pointer-Arithmetik mit religiösem Beistand, sondern präzise Handle- und Policy-Semantik.
2. HandleId ist technische Identität¶
HandleId schützt gegen stale handles / ABA-Probleme.
Er sagt aber allein nicht genug über die Semantik des Handles.
Semantisch wichtige Fragen:
Was liegt hinter dem Handle?
Welche Memory-Policy gilt?
In welcher Arena oder welchem Pool liegt das Objekt?
Ist der Zugriff strong oder weak?
Darf der Wert kopiert, bewegt oder geteilt werden?
Darf er Task-Grenzen überschreiten?
Diese Bedeutungen gehören in Type Engine + Tags + Meta-Records, nicht als lose Kommentare in D-Code.
3. JME-Schichten¶
JDL Source
|
| type RequestData : struct { ... }
| :> Memory(Arena[RequestArena])
v
Type Engine
|
| normalisiert Meta
| beweist Labels
v
Generator
|
| entscheidet Register-/Handle-Repräsentation
v
VM
|
| transportiert HandleId bei JME-Werten
v
JME
|
| prüft index + generation
| verwaltet Slot, Refcount, Arena, Pool
v
Objekt
4. Tags für JME-Regionen¶
tag ArenaKind
tag RequestArena : ArenaKind
tag AnalysisArena : ArenaKind
tag PoolKind
tag ConnectionPool : PoolKind
tag ParserNodePool : PoolKind
Verwendung:
type RequestData : struct {
headers: Map[str, str]
body: str
} :> Memory(Arena[RequestArena])
type DbConnection : struct {
native: NativeDbConnection
} :> Memory(Pool[ConnectionPool])
RequestArena und ConnectionPool sind keine Runtime-Werte. Sie markieren Speicherklassen.
5. Memory-Policies¶
Empfohlene Tag-/Type-Level-Form:
Parametrisierte Policies wie Arena[RequestArena] und Pool[ConnectionPool] sind keine einfachen Tag-Varianten, sondern Type-Level-Policy-Ausdrücke.
Konzeptionell:
Syntax bleibt JDL-typisch:
:> Memory(Value)
:> Memory(Rc)
:> Memory(Weak)
:> Memory(Arena[RequestArena])
:> Memory(Pool[ConnectionPool])
6. Typisierte Handle-Abstraktionen¶
Nicht jeder User-Typ muss explizit einen Handle wrapper enthalten.
Normaler Fall:
Der User schreibt Typ + Policy. Der Compiler/JME entscheidet Handle-Repräsentation.
Explizite Handle-Typen gehören in die Stdlib oder an Systemgrenzen:
type Weak[T] : struct {
id: HandleId
} :> Memory(Value)
type Strong[T] : struct {
id: HandleId
} :> Memory(Value)
type JoinHandle[T] : struct {
id: HandleId
} :> Own(Unique) :> Share(Send)
7. Phantom-Policies an Handles¶
Für spezielle Systemtypen können Tags über Phantom-Parameter gebunden werden.
tag AccessMode : enum =
| ReadOnly
| ReadWrite
| Volatile
| Atomic
type ResourceHandle[T, phantom Access: AccessMode] : struct {
id: HandleId
}
Beispiele:
def readConfig(handle: ResourceHandle[ConfigBlob, ReadOnly]) -> ConfigBlob
def writeBuffer(handle: ResourceHandle[Buffer, ReadWrite], data: Array[u8]) -> ()
def readDeviceRegister(handle: ResourceHandle[RegisterBlock, Volatile], offset: u32) -> u32
Runtime:
Compile-Time:
ResourceHandle[Buffer, ReadOnly]
ResourceHandle[Buffer, ReadWrite]
ResourceHandle[RegisterBlock, Volatile]
nicht austauschbar
8. Trust-Level für Ressourcen¶
tag TrustLevel : enum =
| Untrusted
| Validated
| Trusted
type Buffer[phantom Trust: TrustLevel] : struct {
data: Array[u8]
}
APIs:
def parseRaw(input: Buffer[Untrusted])
-> Result[Buffer[Validated], ParseError]
def execute(input: Buffer[Trusted])
-> Result[(), ExecError]
Damit kann ein unvalidierter Buffer nicht versehentlich an eine gefährliche API gehen.
9. Weak und Option¶
Weak-Referenzen sind nicht-besitzende Handles.
Zugriff:
Mit try-Boundary:
10. Keine rohen Pointer¶
Verboten in JDL:
Ausnahme:
JDL-seitig werden Low-Level-Ressourcen über Handles und Policies modelliert.
11. Normative Invarianten¶
JME-1: JDL kennt keine rohen Pointer.
JME-2: JDL-Code arbeitet mit Werten, Handles und typisierten Ressourcenabstraktionen.
JME-3: HandleId ist technische Runtime-Identität, keine vollständige Semantik.
JME-4: Memory-/Access-/Trust-/Region-Semantik wird durch Tags, Meta-Records und Phantom-Parameter modelliert.
JME-5: Die Type Engine normalisiert JME-Policies vor Codegen.
JME-6: Die JME erzwingt Runtime-Sicherheit über Handle Table + Generation.
JME-7: Die Type Engine erzwingt statische Sicherheit über Labels + Policies.
JME-8: Explizite HandleId-Felder sind System-/Stdlib- oder FFI-nahe Konstrukte und müssen starke Ownership-Labels tragen.
12. Nativer Handle-Zustand¶
Native Handles tragen Zustand, den der Scheduler für die Ausführung braucht, aber nicht besitzen darf. Die JME ist die einzige Stelle, die die Wahrheit über Handles hält. Der Scheduler fragt, die JME antwortet.
12.1 Was statisch ist und was die JME führt¶
Der statische Vertrag eines nativen Handle-Typs steht im FFI-Descriptor und ändert sich zur Laufzeit nie:
Die JME führt pro Handle nur den dynamischen Zustand, und nur dort, wo der Vertrag ihn verlangt:
Handle-Slot (nativ)
affinityDomain: AffinityDomainId? nur bei affinity = stable
exclusiveOwner: NativeEntryToken? nur bei affinity = any und access = exclusive
nativeHolds: Zähler laufender nativer Aufrufe auf diesem Handle
releasing: Freigabe angefordert, aber noch nicht ausgeführt
Die AffinityDomainId ist keine JDL-Semantik. JDL-Code sieht sie nie, sie existiert nur zwischen JME und Scheduler.
12.2 Bindung beim Erzeugen¶
Der FFI-Vertrag eines erzeugenden Aufrufs sagt, wie das neue Handle an eine Domain kommt:
create(result) neue AffinityDomain
inherit(result, from) Domain eines vorhandenen Handles übernehmen
Bei create legt die JME die Domain vor dem Aufruf an, der Scheduler ordnet ihr eine Lane zu und führt den erzeugenden Aufruf dort aus. Erst nach der Rückkehr entsteht der Handle-Slot, mit affinityDomain auf die neue Domain gesetzt.
Bei inherit übernimmt der neue Slot die Domain des angegebenen Handles.
Schlägt der erzeugende Aufruf fehl, entsteht kein Slot. Eine bei create angelegte Domain hat dann keine Handles und wird sofort wieder freigegeben.
Die Domain eines Handles ändert sich während seiner Lebensdauer nie.
12.3 Lebensdauer einer AffinityDomain¶
Eine AffinityDomain lebt, solange mindestens eines gilt:
- ein Handle ist an sie gebunden,
- ein nativer Aufruf in ihr läuft noch.
Endet beides, meldet die JME über den RuntimeBus release affinity domain. Der Scheduler löst daraufhin die Zuordnung zur Lane. Welche Lane es war, weiß nur der Scheduler; die JME speichert keine Lane.
12.4 Exklusiv-Zustand¶
Für Handles mit affinity = any und access = exclusive gilt beim nativen Eintritt:
exclusiveOwner leer → wird auf den eintretenden Token gesetzt
exclusiveOwner anderer Token → JME meldet "belegt"; der Scheduler lässt die Task warten
exclusiveOwner derselbe Token → Trap
Der Übergang von leer auf belegt ist atomar. Beim Austritt setzt die JME exclusiveOwner zurück und meldet die Freigabe, damit der Scheduler eine wartende Task weckt. Die Warteschlange selbst gehört dem Scheduler.
Für Handles mit affinity = stable führt die JME keinen Exklusiv-Zustand. Die Lane serialisiert die Aufrufe bereits.
12.5 Holds und verzögerte Freigabe¶
Jeder laufende native Aufruf erhöht nativeHolds der beteiligten Handles und hält ihre Domain und Bibliothek am Leben.
Wird ein Handle freigegeben, während noch ein nativer Aufruf auf ihm läuft, etwa weil die aufrufende Task abgebrochen wurde und ihr Besitzer das Handle aufgibt:
- Die JME setzt
releasing. Ab jetzt werden neue Zugriffe abgewiesen, als wäre das Handle bereits veraltet. - Der laufende Aufruf darf zu Ende laufen.
- Fällt
nativeHoldsauf null, führt die JME die Freigabe aus, erhöht die Generation und gibt den Slot frei.
Damit entsteht kein Zeitfenster, in dem jemand ein halb freigegebenes Handle benutzen kann, und kein nativer Aufruf verliert sein Handle unter den Füßen.
12.6 Freigabe läuft auf der richtigen Lane¶
Die Freigabe eines nativen Handles ist selbst ein nativer Aufruf, etwa sqlite3_close oder das Löschen einer OpenGL-Textur. Sie unterliegt demselben Vertrag wie jeder andere Aufruf auf dem Handle: Bei affinity = stable läuft sie auf der Lane der Domain.
Die JME gibt eine Domain deshalb erst frei, nachdem die Freigabe-Aufrufe aller ihrer Handles dort gelaufen sind.
Das gilt auch auf dem Trap-Pfad. Nach dem Fehlermodell läuft dort kein JDL-Code (F-8). Die Freigabefunktion eines nativen Handles ist aber kein JDL-Code, sondern ein im FFI-Descriptor festgehaltener nativer Aufruf. Die Runtime darf ihn deshalb mechanisch ausführen, auf der richtigen Lane.
12.7 Invarianten¶
JME-9: Die JME ist die einzige Stelle, die AffinityDomain und Exklusiv-Zustand eines Handles hält.
JME-10: Die AffinityDomain eines Handles wird bei seiner Erzeugung festgelegt und ändert sich nie.
JME-11: Eine AffinityDomain lebt, solange ein Handle an sie gebunden ist oder ein nativer Aufruf in ihr läuft.
JME-12: Der Übergang eines exklusiven Handles von frei auf belegt ist atomar; Wiedereintritt mit demselben Token trappt.
JME-13: Ein Handle mit laufenden nativen Aufrufen wird erst nach deren Ende freigegeben; ab der Freigabeanforderung sind neue Zugriffe ungültig.
JME-14: Die native Freigabe eines stabilen Handles läuft auf der Lane seiner Domain, auch auf dem Trap-Pfad.
Änderungsprotokoll¶
2026-09-23 — Abschnitt 12 neu: statischer Vertrag gegen dynamischen Zustand, Bindung beim Erzeugen (create/inherit), Lebensdauer von AffinityDomains, Exklusiv-Zustand, Holds und verzögerte Freigabe, Freigabe auf der richtigen Lane auch auf dem Trap-Pfad. Invarianten JME-9 bis JME-14.