Zum Inhalt

JDL Runtime — Speicher, Ressourcen und Concurrency

Fortsetzung von Teil 2: Typsystem. Unser User-Management-System wird ein Server: Arenas verwalten Request-Daten, with managt Datenbankverbindungen, und Tasks verarbeiten Requests parallel. Jedes Feature kommt dazu wenn die Architektur es verlangt.


1. Speicherverwaltung und Arenas

JDL verwaltet Speicher über ein Handle-basiertes System ohne Garbage Collector. Jeder Wert wird über einen opaken Handle angesprochen, der einen Generation-Counter trägt — Use-After-Free wird zur Laufzeit erkannt, nicht durch Lifetime-Annotations wie in Rust.

Die vier Allokationsstrategien

Jeder Typ hat über sein Meta-Record eine Allokationsstrategie:

// Value — Stack-inline, kein Handle:
type UserStats: struct { 
  totalUsers: i64, activeUsers: i64, avgAge: f64 
} :> Memory(Value)

// Ref — Heap, Handle, Reference-Counted:
type <{ memory: Ref }> User: struct { name: str, email: str, age: i32 }

// Arena — Bump-Allokation, Bulk-Reset:
type <{ memory: Arena[RequestArena] }> RequestData: struct { 
  headers: Map[str, str]
  body: str 
}

// Pool — Wiederverwendung teurer Objekte:
type <{ memory: Pool[ConnectionPool] }> DbConnection: struct { 
  handle: HandleId 
}

Im Normalfall leitet der Compiler die Strategie automatisch ab:

type UserStats: struct { totalUsers: i64, activeUsers: i64, avgAge: f64 }
// Compiler: 24 Bytes, nur Primitives → memory: Value

type User: struct { name: str, email: str, age: i32 }
// Compiler: enthält Handle-Typen (str) → memory: Ref

Erst wenn der Entwickler Kontrolle will, wird memory: explizit.

Arena-Scoping mit with

Arenas werden über with-Blöcke eröffnet und automatisch zurückgesetzt. Unser Server verarbeitet HTTP-Requests — jeder Request bekommt eine eigene Arena für seine kurzlebigen Daten:

// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [UserDb, LogService, Storage] }>
handleListUsers(req: HttpRequest) -> Result[HttpResponse, AppError] {
    with Storage.arena[RequestArena] {
        // Alle Zwischenergebnisse leben in der RequestArena:
        val allUsers =? UserDb.listUsers()
        val filtered = allUsers |> filter(_.age >= 18)
        val sorted   = filtered |> sort(by = _.name)
        val response = sorted |> map(u => u -> UserResponse)

        // Nur das Ergebnis überlebt den Scope:
        buildHttpResponse(response)
        // Return-Wert wird automatisch in die Arena des Aufrufers kopiert
    }
    // RequestArena zurückgesetzt — ein Pointer-Reset, kein Traversal
    // allUsers, filtered, sorted sind vernichtet
}

Arena-Hierarchie

Arenas bilden einen Stack. Innere Arenas werden vor äußeren zurückgesetzt:

// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [UserDb, LogService, Storage] }>
handleUserReport(req: HttpRequest) -> Result[HttpResponse, AppError] {
    with Storage.arena[RequestArena] {
        val users =? UserDb.listUsers()

        // Innere Arena für rechenintensive Statistik:
        val stats = with Storage.arena[AnalysisArena] {
            val raw = users |> map(u => computeUserMetrics(u))
            val aggregated = aggregate(raw)
            // raw wird vernichtet, aggregated wird nach RequestArena kopiert
            aggregated
        }
        // AnalysisArena ist zurückgesetzt

        buildReportResponse(users, stats)
    }
}

Grundregeln

Werte können ihre Arena nicht verlassen — ein Wert kann jedoch in eine andere Arena kopiert werden. Return aus einem with-Block löst diese Kopie automatisch aus.

Werte überqueren Arena-Grenzen immer als Kopie, nie als Referenz. Es gibt keine Cross-Arena-Referenzen. Heap-Objekte (memory: Ref) sind von allen Arenas aus lesbar — sie gehören keiner Arena.

Value-Types (memory: Value) leben inline im Register und brauchen keine Kopie bei Arena-Grenzübergang.

Copy-Mechanismus

Standardmäßig kopiert der JME über den TypeDescriptor — rekursiv, Feld für Feld. Eine Handle-Map bewahrt Sharing und verhindert Endlosschleifen bei Zyklen:

val shared = computeExpensiveStats(users)
val report = Report {
    summaryA: Section { data: shared }
    summaryB: Section { data: shared }    // selber Handle
}
// Nach Copy: summaryA.data und summaryB.data zeigen auf dieselbe Kopie

Drei Stufen der Copy-Kontrolle:

// Level 0: Kein Copyable → JME-Intrinsic (rekursiver Copy über TypeDescriptor)
type UserStats: struct { totalUsers: i64, activeUsers: i64, avgAge: f64 }

// Level 1: derive → generiertes copy() aus Feldern
type <{ derive: [Copyable] }> UserReport: struct { 
  title: str
  users: [User]
  stats: UserStats 
}

// Level 2: provide → custom Logik
type CachedReport: struct { data: UserReport, computedAt: Instant }

provide Copyable for CachedReport {
    def copy(self) -> Self = CachedReport {
        data:       self.data
        computedAt: Instant.now()    // Zeitstempel erneuern statt kopieren
    }
}

Nicht-kopierbare Typen

Manche Typen dürfen nicht kopiert werden — ! im derive-Array negiert ein Protocol:

type <{ derive: [!Copyable] }> AuthToken: struct { id: str, secret: [u8] }

Ein nicht-kopierbarer Typ kann eine Arena nicht via Return verlassen:

Fehler: 'AuthToken' kann den Arena-Scope nicht verlassen.
        Typ hat !Copyable und kann nicht kopiert werden.

   12 | token
        ^^^^^ kann Arena nicht verlassen

Hilfe: Erstelle den Wert außerhalb der Arena,
       oder extrahiere die Daten in einen kopierbaren Typ.

Performance-Profil

Arena Bump-Allokation:              ~5-10ns (Pointer += size)
Heap-Allokation:                    ~100-500ns (Freelist-Scan, Handle-Erstellung)
Arena-Reset (1000 Objekte):         ein Pointer-Reset
Heap-Freigabe (1000 Objekte):       1000× Refcount-Decrement + Drop

Für Request-basierte Workloads — das Backend-Szenario wo JDL glänzen soll — bedeutet das: Dutzende kurzlebige Objekte pro Request werden billig in einer Arena allokiert und am Ende in einem Schlag vernichtet.


2. Ressourcen-Management mit with

with ist JDLs universelles Pattern für deterministisches Cleanup — inspiriert von Pythons Context Manager, aber typsicher über ein Protocol.

Das Scopeable-Protocol

protocol Scopeable[R] {
    def acquire(self) -> Result[R, ScopeError]
    def release(self, resource: R) -> ()
}

with desugared zu acquire/release mit garantiertem Cleanup:

// Syntax:
with expr as name { body }

// Semantisch äquivalent zu:
{
    val name =? expr.acquire()
    val result = { body }
    expr.release(name)
    result
}

Unser Server mit Ressourcen

// Dateien:
// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [Fs] }>
loadUserConfig(path: str) -> Result[UserConfig, AppError] {
    val content = with Fs.open(path) as file {
        file.readText()
    }
    // file geschlossen, content lebt weiter
    parseConfig(content)
}

// Datenbankverbindungen:
// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [Sql, LogService] }>
findUserInDb(email: str) -> Result[User, AppError] {
    val user = with Sql.connect(config.dbUrl) as conn {
        conn.queryOne(f"SELECT * FROM users WHERE email = {email}")
    }
    // connection geschlossen, user lebt weiter
    user
}

// Locks:
def readUserCache(cache: RwLock[Map[str, User]]) -> [User] {
    with cache.readLock() {
        cache.entries() |> map(_.value) |> collect
    }
    // lock released
}

// Arenas:
// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [Storage] }>
analyzeUsers(users: [User]) -> UserReport {
    with Storage.arena[AnalysisArena] {
        val metrics = users |> map(u => computeUserMetrics(u))
        buildReport(metrics)
    }
    // Arena zurückgesetzt, report in Parent-Arena kopiert
}

Immer dasselbe Muster: Ressource öffnen, arbeiten, Ergebnis rausziehen, Ressource schließen.

Ressourcen vs. Daten — die Trennung

with managt Ressourcen (File, Connection, Lock), Arenas managen Daten (Strings, Structs, temporäre Objekte). Die Konsequenz: Arenas enthalten im Normalfall keine Ressourcen mit Custom-Drop.

// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [Fs, Sql, Storage] }>
importUsers(csvPath: str, dbUrl: str) -> Result[UserReport, AppError] {
    with Storage.arena[RequestArena] {

        val users = with Fs.open(csvPath) as file {
            with Sql.connect(dbUrl) as conn {
                val csv = file.readText()
                val parsed = parseCsv(csv)
                parsed |> map(row => insertAndReturn(conn, row)) |> collect
            }
            // conn geschlossen
        }
        // file geschlossen

        // In der Arena leben nur Daten — kein Custom-Drop nötig
        buildReport(users)
    }
    // Arena-Reset: reiner Pointer-Reset, kein Drop-Traversal
}

Diese Trennung eliminiert das Double-Drop-Problem vollständig. Ressourcen werden durch ihr with aufgeräumt, Daten durch die Arena.

Scope-basiertes Cleanup ohne with

Für Typen die Disposable sind, fügt der Compiler automatisch Cleanup am Scope-Ende ein:

// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [Fs] }>
readUserFile(path: str) -> Result[str, IoError] {
    val file =? Fs.open(path)
    // Compiler registriert file's Drop-Sequenz

    val content =? file.readAll()
    val result = processContent(content)
    Ok(result)
    // Scope endet — file.drop() automatisch
    // Auch bei early return via =? wird aufgeräumt
}

with ist die explizite Variante wenn man den Scope bewusst eingrenzen will — z.B. um eine Connection früher freizugeben.


3. Concurrency

Unser User-Server muss Requests parallel verarbeiten. JDLs Concurrency-Modell ist vollständig typsicher: share- und own-Policies arbeiten mit dem Compiler zusammen, sodass Datenraces und ungültige Cross-Task-Zugriffe Compile-Fehler sind.

Überblick

Situation Werkzeug
Unveränderlicher geteilter Zustand share: Sync direkt, kein Lock
Kommunikation zwischen Tasks Channel[T]
Komplexer eigener veränderlicher State Actor-Pattern
Geteilte Mutation, einfach Mutex[T]
Lese-intensiv, seltene Schreibvorgänge RwLock[T]
Häufige Zähler, Flags Atomic[T]
Cross-Process-Kommunikation IPC + WireSafe

Tasks und spawn

val handle: JoinHandle[UserReport] = spawn {
    generateUserReport(allUsers)
}
val report =? handle.join()

JoinHandle[T] ist own: Unique, share: Send — genau ein Besitzer, der Handle kann in einen anderen Task bewegt werden. join konsumiert den Handle.

Own- und Share-Policies

share steuert wer auf einen Wert zugreifen darf. own steuert wie viele logische Besitzer existieren:

// own: Unique — genau ein Besitzer; Übergabe = Move:
type <{ own: Unique, share: Send }> DbConnection: struct { 
  handle: HandleId 
}

// own: Shared — refgezählt (Arc-Semantik):
type <{ own: Shared, share: Sync }> SharedConfig: struct { 
  host: str, port: u16, dbUrl: str 
}


// share: Local — verlässt den erzeugenden Task nie:
type <{ own: Unique, share: Local }> RequestConn: struct { handle: HandleId }
Policy Bedeutung
share: Local Nur im erzeugenden Task — niemals crossTask
share: Send Kann in anderen Task moved werden, dann nicht mehr hier
share: Sync Gleichzeitig aus mehreren Tasks nutzbar (erfordert interne Sync)
own: Unique Genau ein logischer Besitzer — Move-Semantik
own: Shared Mehrere Besitzer, refgezählt (Arc-Semantik)
own: Weak Nicht-besitzende Referenz auf Shared — kein Refcount, Zugriff via upgrade() -> Option[T]

own: Weak verhindert Referenzzyklen: zwei Shared-Werte die sich gegenseitig referenzieren würden den Refcount nie auf null bringen. Hält stattdessen einer Weak, kann der andere dropped werden — und upgrade() gibt dann None zurück.

Fehler: RequestConn hat share: Local — darf nicht in spawned Task gecaptured werden.
  42 | spawn { useConnection(conn) }
                             ^^^^ share: Local

Copy vs. Move bei Task-Grenzen

Ob ein Wert beim Überqueren einer Task-Grenze kopiert oder bewegt wird, ergibt sich aus der Kombination von share und Copyable:

share: Local                → kann Task nicht verlassen
share: Send + Copyable      → wird kopiert in Ziel-Task
share: Send + !Copyable     → wird MOVED (Quelle verliert Zugriff)
share: Sync + Copyable      → kann kopiert oder gleichzeitig gelesen werden

Der interessante Fall ist Send + !Copyable — ein Move:

type <{ share: Send, derive: [!Copyable] }> AuthToken: struct { 
  id: str
  secret: [u8] 
}

// Task A
val token = createAuthToken(userId)
sender.send(token)
// token ist jetzt in Task A ungültig — Compiler verhindert weiteren Zugriff

// Task B empfängt
match receiver.recv() {
    | Message(token) => authenticateWith(token)    // token lebt jetzt hier
    | Closed         => ()
    | Cancelled      => ()
}

Move-Semantik gilt überall wo Werte Task-Grenzen überqueren: spawn- Captures, Channel-Sends und Actor-Nachrichten.

Cancellation als Task-/Channel-Semantik

Die rohe VM-Cancellation ist kooperativ. In der Stdlib wird sie als explizite Outcome-Semantik sichtbar gemacht:

type TaskOutcome[T]: enum =
    | Ready(T)
    | Cancelled

type ReceiveOutcome[T]: enum =
    | Message(T)
    | Closed
    | Cancelled

Wichtig: Closed ist eine Stdlib-Channel-Semantik (z. B. alle Sender sind gedroppt). Cancelled ist eine Task-/Runtime-Semantik. Beide Faelle duerfen nicht still in denselben Sentinel-Wert zusammenfallen.


Channels — typisierte Nachrichtenkanäle

Unser Server braucht Worker-Threads die User-Registrierungen verarbeiten:

type RegistrationMsg: enum =
    | Register { name: str, email: str, age: i32, replyTo: Sender[Result[User, UserError]] }
    | Shutdown

val sender, receiver = Channel[RegistrationMsg].new()
// Sender[T] ist own: Shared, share: Sync — mehrere Tasks können senden
// Receiver[T] ist own: Unique, share: Send — genau ein Empfänger

// Worker-Task:
val worker = spawn {
    loop {
        match receiver.recv() {
            | Message(RegistrationMsg.Register { name, email, age, replyTo }) => {
                val result = registerUser(name, email, age)
                replyTo.send(result)
            }
            | Message(RegistrationMsg.Shutdown) => break
            | Closed     => break    // alle Sender geschlossen
            | Cancelled  => break    // Worker kooperativ abgebrochen
        }
    }
}

// Client sendet Registrierung:
val replySender, replyReceiver = Channel[Result[User, UserError]].new()
sender.send(RegistrationMsg.Register {
    name = "Alice", email = "alice@example.com", age = 30,
    replyTo = replySender
})
val result = replyReceiver.recv()    // Message(Ok(User { ... }))

Locking-Primitive

Mutex[T] — exklusiver Zugriff

Status-Hinweis: Mutex[T], RwLock[T] und Atomic[T] sind in diesem Dokument derzeit Stdlib-Oberflaechenmodelle. Die atomare VM-Unterlage fuer diese Typen ist noch nicht normativ in 08-intrinsics-vm-api.md festgelegt und bleibt bis dahin offen.

Unser Server braucht einen Request-Counter:

type ServerStats: struct { requests: i64, errors: i64, lastUser: Option[str] }

val stats: Mutex[ServerStats] = Mutex.new(ServerStats {
    requests: 0, errors: 0, lastUser: None
})

// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [UserDb, LogService] }>
handleRequest(req: HttpRequest) -> Result[HttpResponse, AppError] {
    stats.modify(s => ServerStats {
        requests: s.requests + 1
        errors:   s.errors
        lastUser: s.lastUser
    })

    val result = processRequest(req)

    match result {
        | Err(_) => stats.modify(s => ServerStats {
            requests: s.requests
            errors:   s.errors + 1
            lastUser: s.lastUser
        })
        | Ok(response) => stats.modify(s => ServerStats {
            requests: s.requests
            errors:   s.errors
            lastUser: Some(response.userName)
        })
    }

    result
}

// Lesen:
val current = stats.read(s => f"Requests: {s.requests}, Errors: {s.errors}")

Warum Closures statt Guards? mutex.lock() gibt in anderen Sprachen ein Guard-Objekt zurück, das man vergessen oder aus dem Scope schmuggeln kann. Die Closure-Form hat keinen dieser Mängel: der Lock existiert genau so lange wie f läuft.

RwLock[T] — viele Leser, ein Schreiber

Unser User-Cache wird häufig gelesen, selten geschrieben:

val userCache: RwLock[Map[str, User]] = RwLock.new(Map.new())

// Viele Tasks lesen gleichzeitig:
// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [LogService] }>
lookupUser(email: str) -> Option[User] =
    userCache.read(cache => cache.get(email))

// Selten — sperrt alle Leser kurz:
// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [LogService] }>
updateUserCache(user: User) -> () =
    userCache.write(cache => cache.insert(user.email, user))
Mutex[T] RwLock[T]
Lese-Muster Exklusiv Concurrent
Schreib-Muster Exklusiv Exklusiv
Overhead Geringer Etwas höher
Typischer Use Case Counter, Queue Cache, Config

Atomic[T] — wartesperre-freie Primitive

Für einfache Zähler — kein Lock, kein Kontext-Switch:

val requestCount: Atomic[u64] = Atomic.new(0u64)
val errorCount:   Atomic[u64] = Atomic.new(0u64)

// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [UserDb] }>
handleRequest(req: HttpRequest) -> Result[HttpResponse, AppError] {
    requestCount.fetchAdd(1)
    val result = processRequest(req)
    match result {
        | Err(_) => errorCount.fetchAdd(1)
        | _      => ()
    }
    result
}

LogService.info(f"Requests: {requestCount.load()}, Errors: {errorCount.load()}")

Actor-Pattern — State-Isolation ohne Locks

Unser User-Service hat komplexen State — ein Actor kapselt ihn sauber:

// State — vollständig privat, verlässt den Actor-Task nie:
type UserManagerState: struct {
    users:      Map[str, User]
    nextId:     i64
    stats:      UserStats
} :> Share(Local)

// Protokoll — die vollständige externe Schnittstelle:
type UserManagerMsg: enum =
    | Register { name: str, email: str, age: i32, replyTo: Sender[Result[User, UserError]] }
    | Find     { email: str, replyTo: Sender[Option[User]] }
    | Stats    { replyTo: Sender[UserStats] }
    | Shutdown

def spawnUserManager() -> Sender[UserManagerMsg] {
    val sender, receiver = Channel[UserManagerMsg].new()
    var state = UserManagerState {
        users:  Map.new()
        nextId: 1
        stats:  UserStats { totalUsers: 0, activeUsers: 0, avgAge: 0.0 }
    }

    spawn {
        loop {
            match receiver.recv() {
                | Message(UserManagerMsg.Register { name, email, age, replyTo }) => {
                    if state.users.contains(email) {
                        replyTo.send(Err(UserError.AlreadyExists { email }))
                    } else {
                        val user = User { name, email, age }
                        state.users = state.users.insert(email, user)
                        state.nextId = state.nextId + 1
                        state.stats = updateStats(state.stats, user)
                        replyTo.send(Ok(user))
                    }
                }
                | Message(UserManagerMsg.Find { email, replyTo }) =>
                    replyTo.send(state.users.get(email))
                | Message(UserManagerMsg.Stats { replyTo }) =>
                    replyTo.send(state.stats)
                | Message(UserManagerMsg.Shutdown) => break
                | Closed     => break
                | Cancelled  => break
            }
        }
    }

    sender
}
// Nutzung:
val userManager = spawnUserManager()

// Fire-and-forget — non-blocking:
val replySender, replyReceiver = Channel[Result[User, UserError]].new()
userManager.send(UserManagerMsg.Register {
    name = "Alice", email = "alice@example.com", age = 30,
    replyTo = replySender
})
val result = replyReceiver.recv()    // Message(Ok(User { ... }))

// Stats abfragen:
val statsSender, statsReceiver = Channel[UserStats].new()
userManager.send(UserManagerMsg.Stats { replyTo = statsSender })
val stats = statsReceiver.recv()     // Message(UserStats { ... })
Kriterium Actor Mutex[T]
State-Komplexität Hoch Niedrig
Deadlock-Risiko Keins Möglich bei Nestung
Request-Response Nativ via Channels Umständlich
Typische Anwendung UserManager, Zustandsmaschinen Counter, Puffer

Arenas und Tasks

Werte die in einer Request-Arena allokiert wurden, dürfen nie in einen langlebigen Task gecaptured werden:

// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [UserDb, Storage] }>
handleRequest(req: HttpRequest) -> Result[HttpResponse, AppError] {
    with Storage.arena[RequestArena] {
        val userData =? UserDb.getUserById(req.userId)

        // COMPILERFEHLER: userData ist in RequestArena — spawn überlebt die Arena
        val handle = spawn { generateReport(userData) }

        // Korrekt — kopierbaren Snapshot erstellen:
        val snapshot = UserSnapshot {
            name:  userData.name
            email: userData.email
        } :> Share(Send)

        val handle = spawn { generateReport(snapshot) }    // ok
        val report =? handle.join()
        buildResponse(report)
    }
}

Faustregeln: - Request-Handler → RequestArena, alles Local - Background-Worker → eigene Arena, nur Send-kompatible Daten - Channels und JoinHandle → immer außerhalb von Request-Arenas


IPC — Cross-Process-Kommunikation

protocol WireSafe { }

// WireSafe wenn alle Felder primitiv oder ebenfalls WireSafe:
type UserSnapshot: struct { name: str, email: str, active: bool }
// → WireSafe: str, bool sind primitive Wire-Typen

// NICHT WireSafe — prozess-lokale Ressource:
type <{ share: Local }> RequestConn: struct { handle: HandleId }

// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [Ipc] }>
sendToAnalytics(conn: IpcConnection, data: UserSnapshot) -> Result[(), IpcError]

sendToAnalytics(conn, requestConn)    // COMPILERFEHLER: nicht WireSafe

4. extern — FFI Bridges

Wenn unser Server eine C-Bibliothek für Kryptographie braucht:

extern Crypto {
    def hashPassword(pwd: ptr[u8], len: u64) -> ptr[u8]
        :> CSymbol("crypto_pwhash_str")

    def verifyPassword(hash: ptr[u8], pwd: ptr[u8], len: u64) -> i32
        :> CSymbol("crypto_pwhash_str_verify")
} :> FFILink("sodium")

Relation zu service

// Abstrakte Capability — in deps verwendet:
service PasswordHasher {
    def hash(password: str)                  -> Result[str, CryptoError]
    def verify(password: str, hash: str)     -> Result[bool, CryptoError]
}

// Konkrete FFI-Implementierung:
extern Crypto {
    def hash(pwd: ptr[u8], len: u64) -> ptr[u8]
        :> CSymbol("crypto_pwhash_str")
} :> FFILink("sodium")

// Service-Zuordnung separat (implements existiert nicht):
provide PasswordHasher for SodiumHandler { ... }
Konzept Ebene Zweck
service Logik-Schicht Abstrakte Capability in deps
extern Kompositions-Schicht Konkrete FFI-Brücke zu C
provide Service[Handler] Logik-Schicht JDL-Implementierung eines service

Nur FFI-sichere Typen in def innerhalb von extern-Blöcken: primitive Skalare, ptr[T], Structs mit repr: C, Enums ohne Payload. Closures, Arena-gebundene Typen und share: Local-Typen sind verboten.


5. Vollständiges Beispiel — User-Management-Server

Alles zusammen — unser System aus drei Dokumenten als lauffähiger Server:

// === Domänentypen (Teil 1) ===

type UserId = UserId(str)

type Role: enum =
    | User
    | Admin
    | Moderator

type User: struct {
    id:    UserId
    name:  str
    email: str
    age:   i32
    role:  Role
}

type UserError: enum =
    | NotFound    { id: str }
    | InvalidEmail { email: str, reason: str }
    | AlreadyExists { email: str }

type AppError: enum =
    | UserErr(UserError)
    | DbErr { message: str }

// === Typ-Konversionen (Teil 2) ===

type UserResponse: struct { id: UserId, name: str, email: str, role: Role }

provide CastTo[UserResponse] for User {
    def castTo(self) -> UserResponse = UserResponse {
        id:    self.id
        name:  self.name
        email: self.email
        role:  self.role
    }
}

provide CastTo[AppError] for UserError {
    def castTo(self) -> AppError = AppError.UserErr(self)
}

// === Services (Teil 1) ===

service UserDb {
    def getUserById(id: UserId)       -> Result[User, UserError]
    def getUserByEmail(email: str)    -> Result[User, UserError]
    def insertUser(user: User)        -> Result[User, UserError]
    def listUsers()                   -> Result[[User], UserError]
}

service LogService {
    def debug(msg: str) -> ()
    def info(msg: str)  -> ()
    def warn(msg: str)  -> ()
    def error(msg: str) -> ()
}

// === Validierung (Teil 1) ===

def validateEmail(email: str) -> Result[str, UserError] {
    if not email.contains("@") then
        Err(UserError.InvalidEmail { email, reason: "Kein @-Zeichen" })
    else
        Ok(email)
}

// === Geschäftslogik ===

/// Registriert einen neuen Benutzer.
/// =? propagiert UserError, CastTo[AppError] for UserError greift automatisch.
// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [UserDb, LogService] }>
registerUser(
    name:  str
    email: str
    age:   i32
    role:  Role = Role.User
) -> Result[UserResponse, AppError] {
    val validEmail =? validateEmail(email)

    val user = User {
        id:    UserId(f"u-{Instant.now().millis()}")
        name:  name
        email: validEmail
        age:   age
        role:  role
    }

    val saved =? UserDb.insertUser(user)
    LogService.info(f"Registriert: {saved.name} <{saved.email}>")

    match saved.role {
        | Role.Admin => LogService.warn(f"Admin erstellt: {saved.name}")
        | _          => ()
    }

    // Ok(saved) — Compiler wendet CastTo[UserResponse] for User an
    Ok(saved)
}

/// Listet erwachsene User, sortiert nach Name.
// TODO: deps-Syntax veraltet — konsolidieren zu imperativem oder Effect-Stil
def<{ deps: [UserDb, Storage] }>
listAdultUsers() -> Result[[UserResponse], AppError] {
    with Storage.arena[RequestArena] {
        val users =? UserDb.listUsers()
        val result =
            users
            |> filter(u => u.age >= 18)
            |> sort(by = _.name)
            |> map(u => u -> UserResponse)
        Ok(result)
    }
}

// === Concurrency — Server-Stats ===

val requestCount: Atomic[u64] = Atomic.new(0u64)

// === Deployment-Wiring ===

CallGraph app(req: HttpRequest) -> Result[HttpResponse, AppError] {
    requires: [UserDb, LogService, Storage]

    env: {
        UserDb:     PostgresUserDb { connectionString: config.dbUrl }
        LogService: ConsoleLogger { level: LogLevel.Info }
        Storage:    JmeStorage { defaultArenaSize: 64_000 }
    }

    Node route  { call: routeRequest(req) }
    Node handle { call: handleRoute(route.result) }
    flow: route -> handle
}

Zusammenfassung Teil 3

Speicher:         Vier Strategien: Value, Ref, Arena, Pool
                  Handle-basiert mit Generation-Counter
                  Compiler leitet memory:-Policy automatisch ab
                  Arenas via with-Block, Bulk-Reset am Scope-Ende
                  Copyable für Custom-Copy, !Copyable für Negation

Ressourcen:       with expr as name { body } — deterministisches Cleanup
                  Scopeable[R]: acquire/release
                  Trennung: with für Ressourcen, Arenas für Daten

Concurrency:      spawn, JoinHandle[T]
                  Channel[T] → Sender[T] (Sync) + Receiver[T] (Send)
                                 recv() → Message | Closed | Cancelled
                  Task[T]    → await() → Ready | Cancelled
                  Mutex[T].read/modify, RwLock[T].read/write
                  Atomic[T].load/store/fetchAdd/compareSwap
                  Actor-Pattern: Local-State + Channel + spawn
                  IPC + WireSafe für Cross-Process
                  own: Unique | Shared,  share: Local | Send | Sync
                  Copy vs. Move: Send+Copyable=Copy, Send+!Copyable=Move

FFI:              extern, def (in extern-Block), :> CSymbol, :> FFILink