JDL Runtime — Speicher, Ressourcen und Concurrency¶
Fortsetzung von Teil 2: Typsystem. Unser User-Management-System wird ein Server: Arenas verwalten Request-Daten,
withmanagt 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:
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]undAtomic[T]sind in diesem Dokument derzeit Stdlib-Oberflaechenmodelle. Die atomare VM-Unterlage fuer diese Typen ist noch nicht normativ in08-intrinsics-vm-api.mdfestgelegt 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