Zum Inhalt

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:

- Werten
- Handles
- typisierten Runtime-Abstraktionen
- compile-time Policies

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 = { index: u32, generation: u32 }

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:

tag MemoryKind : enum =
    | Value
    | Rc
    | Weak
    | Arena
    | Pool

Parametrisierte Policies wie Arena[RequestArena] und Pool[ConnectionPool] sind keine einfachen Tag-Varianten, sondern Type-Level-Policy-Ausdrücke.

typefn Memory(policy: MemorySpec) = <{ memory: policy }>

Konzeptionell:

MemorySpec
 ├─ Value
 ├─ Rc
 ├─ Weak
 ├─ Arena[ArenaKind]
 └─ Pool[PoolKind]

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:

type User : struct {
    name: str
    email: str
} :> Memory(Rc)

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:

ResourceHandle[...] -> HandleId

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.

type Node : struct {
    value:    i32
    parent:   Weak[Node]
    children: Array[Strong[Node]]
}

Zugriff:

match node.parent.upgrade() {
    | Some(parent) => use(parent)
    | None         => handleMissingParent()
}

Mit try-Boundary:

val parentOpt = try {
    val parent =? node.parent.upgrade()
    parent
}

10. Keine rohen Pointer

Verboten in JDL:

type Ptr[T] : struct { addr: usize }

Ausnahme:

FFI- oder Host-Implementierungsdetails in D.
Nicht als normales JDL-Sprachkonstrukt.

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:

access:    shared | exclusive
affinity:  any | stable

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:

  1. Die JME setzt releasing. Ab jetzt werden neue Zugriffe abgewiesen, als wäre das Handle bereits veraltet.
  2. Der laufende Aufruf darf zu Ende laufen.
  3. Fällt nativeHolds auf 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.