Zum Inhalt

Design-Notiz: Jade Engine-Architektur

Status: Informativer Entwurf — Designnotizen und Architekturentscheidungen Geltungsbereich: Engine-Konzept, Blueprint-Semantik, Stdlib-Architektur, Intrinsics-Integration, I/O-Modell, Self-Hosting, JadeOS-Vision Bezieht sich auf: TypeFn-Spec, VM-Scheduler-Spec, Blueprint-Spec, Ontologie, CompilerDB-Spec, Intrinsics & VM API (08) Autor: Elias

Konsolidierungshinweis: Diese Notiz wurde an Constitution-as-Descriptor, JME-zentrierten Ring 0, Ring-1-jade:: und Ring-2-jdl:: angepasst. Wo ältere Kurzformen wie Engine[Spec, Out] stehen, gelten sie nur noch ergonomisch; normativ ist die Descriptor-Mitte.


1. Kern-These

Komplexität gehört in die Maschine, nicht zum User.

Jade trennt Beschreibung von Ausführung. Der User beschreibt was passieren soll (Blueprint). Die Maschine bestimmt wie es passiert (Engine). Das Typsystem stellt sicher dass beides zusammenpasst (TypeFn + Refinements). Zur Laufzeit verschwindet die gesamte Typ-Ebene — übrig bleiben Bytecode-Instruktionen und ein Scheduler der sie abarbeitet.


1.1 Neue Architekturformel

D / Ring 0      = JME-zentrierter Maschinenkern + Trust Anchor
JDL / Ring 1    = jade:: Semantik + Descriptor-/Provider-Schicht
JDL / Ring 2    = jdl:: Stdlib-Systemschicht + Userland
Blueprint       = deklarative Beschreibung
Engine          = Auswertung dieser Beschreibung
Descriptor      = materialisierter Vertrag zwischen JDL-Semantik und Ring-0-Domäne

Engines erzeugen zur Compile-Zeit oder Setup-Zeit Descriptoren. Ring-0-Domänen, vor allem die JME als Integritätsanker, verwalten zur Laufzeit Instanzen anhand dieser Descriptoren. Die TypeEngine erzeugt also TypeDescriptoren; JME, Scheduler, RuntimeBus und VM konsumieren diese Descriptoren. Die TypeEngine verwaltet keine Runtime-Typen direkt.

Diese Grenze ist die Self-Hosting-Grenze: so viel Semantik wie möglich liegt in JDL, so wenig Mechanik wie nötig bleibt in D.

2. Das Engine-Konzept

2.1 Definition

Eine Engine ist eine typisierte Auswertungsinstanz für einen Blueprint- oder Descriptor-Subject. Sie validiert ein Subject gegen Engine-spezifische Regeln und erzeugt daraus ein materialisiertes Artefakt: entweder einen Compile-Zeit-Descriptor oder ein Runtime-Handle.

Eine Engine besitzt keine Ring-0-Subsystemzustände. Ring-0-Zustände bleiben Eigentum ihrer Domänen. Engines erzeugen Descriptoren und Konfigurationen, die von Ring-0-Domänen konsumiert werden.

Das Pattern existiert in vielen Systemen unter verschiedenen Namen:

Jade           Blueprint + Engine           → Handle
Erlang/OTP     Behaviour-Modul + gen_server → Pid
React          JSX-Komponente + Reconciler  → DOM
Kubernetes     YAML-Manifest + Controller   → Running Pod
Terraform      HCL-Datei + Provider         → Infrastructure
Qt/QML         QML-Dokument + QQmlEngine    → UI
Unity          Scene + Runtime              → Game

Jade macht das Pattern zum First-Class-Sprachkonzept. Der einfache Vertrag lautet weiterhin Engine[Spec, Out], die normative Ausführung läuft aber über eine materialisierte Mitte:

protocol DescriptorEngine[Spec, Desc] {
    def describe(spec: Spec) -> Result[Desc, DiagnosticSet]
}

protocol RuntimeEngine[Spec, Desc, Out] {
    def describe(spec: Spec) -> Result[Desc, DiagnosticSet]
    def instantiate(desc: Desc) -> Result[Out, EngineError]
}

describe ist Compile-Zeit- oder Setup-Semantik. instantiate ist Runtime-Semantik. Out ist typischerweise ein Handle, eine Referenz oder ein fertiges Artefakt.

2.2 Generische Engines

Engines sollten generell gehalten werden. Typparameter für austauschbare Aspekte, separate Engines nur bei fundamental verschiedenen Ausführungsmodellen.

TransportEngine[Http]       statt HttpTransportEngine
TransportEngine[Grpc]       statt GrpcTransportEngine
ParserEngine[Json]          statt JsonParserEngine
StorageEngine[Postgres]     statt PostgresEngine

Faustregel: wenn Engines dieselbe Grundstruktur teilen und sich nur in einem Aspekt unterscheiden → generisch. Wenn die Ausführungsmodelle fundamental verschieden sind (RenderEngine vs. BatchProcessor) → separat.

2.3 Engine als Task-Gruppe

Für den Scheduler ist eine Engine eine Konfiguration in einer Lookup-Tabelle (vergleichbar mit Linux cgroups). Kein Thread, kein Prozess. Jeder Task trägt eine engineId, der Scheduler schlägt die Config nach. Details: VM-Scheduler-Spec, Section 6.

2.4 Engine-Lifecycle (State Machine)

stateDiagram-v2
    [*] --> Defined: type MyService : blueprint
    Defined --> Instantiated: val spec = MyService { ... }
    Instantiated --> Described: engine.describe(spec) → Descriptor
    Described --> Running: engine.instantiate(descriptor) → Handle
    Running --> Paused: handle.pause()
    Paused --> Running: handle.resume()
    Running --> ShuttingDown: handle.shutdown()
    Paused --> ShuttingDown: handle.shutdown()
    ShuttingDown --> Dead: Cleanup
    Dead --> [*]

Jeder Übergang hat Guards und Actions. Der Handle den run() zurückgibt exponiert die verbleibenden Übergänge ab Running.


3. Blueprints

3.1 Struct vs. Blueprint

Structs und Blueprints sind verschiedene TypeKinds mit verschiedener Semantik:

Struct:      Datentyp. Felder, Methoden (via provide).
             Stack, Register-Value oder Handle. Pure Daten.
             Kompatible Refinements für Memory, Ownership, Share, Drop, Derive, Operatoren.

Blueprint:   Ausführungsbeschreibung. Slots für Werte, Funktionsreferenzen oder Sub-Blueprints.
             Wird von einer Engine interpretiert. Dictionary-artiges Subject.
             Runtime-orchestrierende Refinements konfigurieren Engine-, Scheduler- und Lifecycle-Verhalten.

3.2 Blueprint-Felder

Ein Blueprint-Feld ist entweder ein Wert (Konfiguration, Tags), ein anderer Blueprint (inline oder extern), oder eine Funktionsreferenz beziehungsweise ein Handler-Slot. Funktionen in Blueprints sind qualifizierte Top-Level-Funktionen oder capture-freie Lambdas.

type ApiService : blueprint {
    // Sub-Blueprint
    db: DbPoolSpec { connection: "postgres://...", poolSize: 10 }

    // Handler-Slots (capture-frei; Abhängigkeiten laufen über Parameter/Felder)
    getUser:    (UserId) -> Result[User, ApiError] =
        (id) => db.findById(id)

    createUser: (CreateUserRequest) -> Result[User, ApiError] =
        (req) => db.insert(User.from(req))
}

Handler-Funktionen beziehen ihre Daten über Parameter und explizite Blueprint-Felder. Ein Blueprint besitzt keinen lexikalischen Capture-Scope. Dadurch bleibt er serialisierbar und remote ausführbar.

3.3 Refinements auf TypeSubjects

Refinements gelten auf kompatiblen TypeSubjects. Structs, Primitive, Functions, Protocols und Blueprints besitzen jeweils Meta-Records, aber nicht jedes Refinement ist auf jedem Subject zulässig. Runtime-orchestrierende Refinements gelten ausschließlich auf Blueprint- oder Engine-Subjects. Jedes Refinement ist eine validierte TypeFn-Anwendung die einen Meta-Record-Eintrag setzt. Details: TypeFn-Spec, Section 3.

type Cache : blueprint {
    entries: [CacheEntry]    // CacheEntry ist ein Struct mit eigener Daten-/Memory-Semantik
    maxSize: u32
} :> Own(Unique) :> Share(Local) :> Queryable()

3.4 Einstiegspunkt

Die VM sucht in main.jdl nach einem von zwei Einstiegspunkten:

// Imperativ: einfache Programme, Scripts, CLI-Tools
def main() -> i32 {
    print("Hallo Welt")
    0
}

// Deklarativ: Apps mit Engines, Blueprints, Lifecycle
type Main : blueprint {
    api:     ApiService { ... }
    db:      DbPoolSpec { ... }
    actors:  ActorSpec  { ... }
} :> ResourceLimits { maxTasks: 10000 }

4. Die run()-Funktion

run(blueprint, engine) ist die fundamentale Aktivierungsoperation. Sie lebt in jade::runtime.

run(blueprint, engine):
    1. Subject normalisieren
    2. materialisierte Refinement-/Meta-Descriptoren lesen
    3. Engine-Kompatibilität über Descriptoren prüfen
    4. EngineDescriptor erzeugen
    5. SchedulerConfig aus Engine-Defaults + Refinements mergen
    6. CapabilitySet prüfen
    7. Middleware-Chain aus MetaConsumer-Provides bauen
    8. Runtime-Descriptor/Config materialisieren
    9. engine.instantiate(descriptor) durch Middleware-Chain
   10. Handle zurückgeben

Die Engine berührt den Scheduler nicht direkt. run() liest bereits materialisierte Refinement-/Meta-Descriptoren sowie Engine-Defaults, merged daraus die Runtime-Konfiguration und registriert diese beim Scheduler.

4.1 Middleware als Lifecycle-Hooks

Refinements die Laufzeitverhalten konfigurieren, werden als Middleware an Engine-Lifecycle-Hooks registriert:

LifecycleHook.AroundExecute     Retry, Timeout, CircuitBreaker
LifecycleHook.BeforeExecute     WarmupCache, PreloadData
LifecycleHook.AfterExecute      Metrics, Logging
LifecycleHook.OnShutdown        GracefulDrain, ConnectionClose
LifecycleHook.OnPause           SuspendTimerWheel, StopAccepting
LifecycleHook.OnResume          ResumeTimerWheel, StartAccepting

Jedes Refinement hat ein provide MetaConsumer das den Hook und das Verhalten definiert. run() liest die materialisierten MetaConsumer-Descriptoren aus der CompilerDB und registriert sie. Details: TypeFn-Spec, Section 8.

4.2 Dependency-Auflösung

Blueprint-Felder die andere Blueprints referenzieren, erzeugen implizite Abhängigkeiten. run() analysiert den Blueprint-Baum und startet Sub-Blueprints in topologischer Reihenfolge.

Main-Blueprint:
    db referenziert nichts      → startet zuerst
    cache referenziert nichts   → startet parallel zu db
    api referenziert db         → startet nach db

5. Namespace-Zugriffshierarchie

Vier Ebenen, Constitution-enforced:

app::               → darf jdl:: importieren
jdl::               → darf jade:: importieren
jade::              → darf jade::intrinsics:: importieren
jade::intrinsics::  → spricht mit dem RuntimeBus (D)

5.1 Was wo lebt

jdl::       WAS existiert      Blueprints, Protocols, Typen, Services, user-facing Engines
jade::      WIE es systemnah funktioniert: Ring-1-Provider, Descriptoren, Intrinsic-Wrapping

jdl::fs          FsSpec : blueprint              Beschreibung
jade::fs         provide Engine[FsSpec, ...] { intrinsic::... }  Implementierung

jdl::concurrent  AtomicCounter, Atomic[T]        Abstraktionen
jade::atomic     provide Atomic for ... { intrinsic::... }       Intrinsic-Wrapping

Faustregel: enthält ein provide das Wort intrinsic::, gehört es nach jade::.

5.2 Umfang

jade::     ~8 Module, ~200 Provides, ~2.000–4.000 SLOC (dünnste Schicht)
jdl::      ~100.000+ SLOC (Bulk der Stdlib, reines JDL)

6. CompilerDB und RuntimeBus — die zwei Zugangspunkte

TypeFns sind die Compile-Zeit-Schnittstelle zur CompilerDB. Intrinsics sind die Runtime-Schnittstelle zum RuntimeBus. Beides ausschließlich über die Stdlib.

Compile-Zeit:    typefn  →  db.*        →  CompilerDB
Runtime:         def     →  intrinsic:: →  RuntimeBus

6.1 Was D sieht

D sieht keine JDL-Semantik. Handles sind Indizes in die Handle-Tabelle. Ein HandleEntry enthält: data (void*), typeId, arenaId, refCount, generation, flags. Die flags sind das einzige was vom Meta-Record durchsickert — Own(Shared) wird zu einem SHARED-Bit, Share(Sync) zu einem SYNC-Bit.

6.2 Handle-Selektion

TypeKind + Meta-Record-Policies bestimmen den Handle-Typ:

struct + Own(Unique) + Value    → Stack-Wert, kein Handle
struct + Own(Unique) + Heap     → Unique-Heap-Handle
struct + Own(Shared)            → Refcounted-Handle
blueprint                       → Dictionary-Handle
tag                             → kein Handle (T-3)

6.3 Intrinsic-Aufruf

Kein JadeValue, kein Bridge, kein Marshalling. Register enthalten rohe Werte. Intrinsic-Handler sind D-Funktionen im selben Prozess. JadeValue lebt nur an den Grenzen: Plugin-API, FFI, REPL, Debugger.

Details zur Intrinsic-ABI (16-Bit-Nummerierung, Stabilitätsgarantien, Opcode-Promotion): VM-Scheduler-Spec, Section 7.


7. Asynchrones I/O

7.1 Event-Loop als VM-Infrastruktur

Der Event-Loop ist ein D-Thread (Ring 0/1), kein JDL-Actor. Er pollt das OS (epoll/kqueue/io_uring), weckt Tasks bei Completion. I/O ist async unter der Haube, synchron an der Oberfläche.

val data = file.read("config.yaml")    // sieht synchron aus
val parsed = parser.parse(data)         // VM hat transparent suspendiert/resumed

Kein async-Keyword, kein await-Operator. Details: VM-Scheduler-Spec, Section 3.3.

7.2 Reaktive Engines

Engines ohne Timer-Wheel sind rein reaktiv: kein Overhead im Leerlauf, Aktivierung nur bei eingehenden Events. Ideal für HTTP-Server, Actor-Supervisoren, Datenbank-Pools.

7.3 Quality Back-Pressure für Streams

Channels zwischen Nodes tracken Latenz implizit über Antwortzeiten. Proaktive Anpassung: wenn die Latenz steigt, senkt der Sender die Qualität bevor die Queue überläuft.

type VideoChannel : blueprint {
    stream: Channel[EncodedFrame]
} :> QualityPolicy {
       levels: [Quality.High, Quality.Medium, Quality.Low]
       degradeWhen: backpressure > 0.7
       recoverWhen: backpressure < 0.3
   }
  :> LatencyTracking(ExponentialDecay(alpha: 0.1))
  :> JitterCompensation(RttVariance)

Clock-Synchronisation zwischen Nodes via Offset-Berechnung im ersten Frame-Exchange.


8. Self-Hosting — Compiler-Phasen als Engines

Compiler-Phasen sind DescriptorEngine- beziehungsweise EngineProvider-Implementierungen:

ParserEngine:         In: Source + ParserSpec → Out: AST
TypeEngine:           In: AST + TypeRules → Out: TypedAST
ConstitutionEngine:   In: ConstitutionSpec + NamespaceScope → Out: ConstitutionDescriptor
GeneratorEngine:      In: LoweredModule + Descriptoren → Out: VmModuleArtifact

Der JDL-Compiler wird als Stdlib-Plugin ausgeliefert — vorkompilierter Bytecode. Der User kompiliert den Compiler nicht, er lädt ihn. VM-Snapshots reduzieren die Startup-Zeit (VM + Stdlib + Compiler im Snapshot eingefroren).

Stage 1:  D implementiert Bootstrapper, Seed-Loader und minimalen Token-Stream-Parser
Stage 2:  JDL definiert Parser-, Type-, Dispatch- und Generator-Engines, die Descriptoren erzeugen
Stage 3:  JDL-Compiler ersetzt den D-Bootstrap als Normalpfad; D bleibt Trust Anchor und Recovery Path

Wer Custom-Compiler braucht (DSL-Parser, Custom-Linter), baut aus denselben Engine-Bausteinen.


9. Testing als Engines

type UserServiceTest : blueprint {
    setup:               () -> TestContext
    getUser_exists:      (ctx) -> Assertion
    getUser_notFound:    (ctx) -> Assertion
} :> Timeout(5.seconds) :> Parallel(maxWorkers: 4)

type UserServicePropTest : blueprint {
    generators: { userId: Gen[UserId], userData: Gen[CreateUserRequest] }
    properties: { roundtrip: (id, data) -> bool }
} :> Iterations(10_000) :> Shrink(enabled: true)

jade test = finde alle Test-Blueprints, starte die passende Engine, sammle Ergebnisse.


10. Transport-agnostische API-Contracts

tag InteractionMode : enum =
    | RequestResponse | ServerStream | ClientStream | Bidirectional | FireAndForget

type Op[In, Out, E, phantom Mode: InteractionMode] : struct {}

type UserService : blueprint {
    getUser:    Op[UserId, User, ApiError, RequestResponse]
    listUsers:  Op[UserFilter, User, ApiError, ServerStream]
}

Derselbe Contract, verschiedene Transport-Engines. Die HandlerMap-TypeFn leitet Handler-Signaturen aus den Op-Phantom-Parametern ab. Details: TypeFn-Spec, Section 6 (Rückgabetypen).


11. Constitution as Descriptor

Namespace-hierarchische, compiler-enforced Architektur-Invarianten. Eine Constitution ist ein deklarativer Blueprint, keine freie Query-Lambda-Sammlung.

ConstitutionSpec
  -> ConstitutionEngine
  -> ConstitutionDescriptor
  -> Enforcement-Gates in ImportResolver, TypeEngine, Materializer, EffectChecker, Verifier

Regeln bestehen aus Trigger, Selector, Predicate und Action. Sie lesen nur definierte CompilerDB-Queries und materialisierte Descriptoren. Monotonie-Regel: jede Ebene darf Constraints hinzufügen oder verschärfen, aber nie bestehende lockern.

Ring0Constitution   sealed, D/JME-enforced
Ring1Constitution   jade::, gegen Ring 0 validiert
Ring2Constitution   jdl:: und User-Packages, nur restriktiv erweiterbar

Die ConstitutionEngine ist eine Ring-1-nahe DescriptorEngine und ein Self-Hosting-Kandidat.


12. JadeOS-Vision

12.1 Drei Stufen

Jade 0.1       Sprache, VM, Stdlib, REPL, Tooling
Jade Node      ausführende Zelle: Runtime, Store, Policy, Engine-Angebote
JadeOS         Mesh aus Jade Nodes, SystemSpecs, Generations, ContentStore

JadeOS ist initial kein eigener Kernel. JadeOS ist die verteilte Betriebsform von Jade Nodes auf minimalen Host-Systemen, initial POSIX/Linux. Ein späterer eigener Kernel oder Bare-Metal-Host ist nicht ausgeschlossen, aber nicht der Kern der Vision.

12.2 Mesh-Architektur

Nodes bieten Engines, Capabilities und Artefakte an. Blueprints und SystemSpecs beschreiben Bedarf. Das Mesh verbindet beides.

Node A: "Ich habe TransportEngine[Http], StorageEngine[Postgres]"
Node B: "Ich habe RenderEngine, ComputeEngine"
User:   mesh.deploy(myApi) → Mesh findet passenden Node → Blueprint/SystemSpec wird geschickt

Die Intrinsic-ABI bleibt die Brücke zum Host. Unter Linux wird ein Intrinsic etwa über epoll/io_uring implementiert; auf einem späteren anderen Host könnte dieselbe ABI anders implementiert werden. Die Schnittstelle bleibt stabil, nicht die konkrete Unterlage.

13. Performance-Schätzungen

Compile-Zeit (50K SLOC, Stdlib gecacht):
    Debug:       5–15s warm, 0.5–3s inkrementell
    Release:     15–45s warm, 2–8s inkrementell

Startup:
    Ohne Snapshot:  50–200ms (VM + Stdlib + App)
    Mit Snapshot:   5–10ms

Runtime:
    Bytecode-Dispatch:  ~5–15x langsamer als C/Rust (CPU-bound)
    I/O-bound:          kaum Unterschied zu nativen Sprachen
    Handle-Overhead:    ~5–20ns pro Refcount-Operation

Scheduler:
    Pro Task:     ~300–600 Bytes
    10.000 Tasks: ~3–6 MB Scheduler-Overhead

14. Offene Punkte

Engine-Lifecycle-Spec        Alle Übergänge, partielles Failure, Reinitialize
Derive-Mechanismus           Engine die Provides aus Meta-Records generiert
Contract-Versionierung       Semver, Wire-Kompatibilität, Migration
Constitution-Komposition     Monotonie-Regel, Lattice-Struktur
Refinement-Discoverability   Constraint-basierte Filterung, IDE-Integration
TypedClient-API              Wie Contract-Ops als Client-Methoden erscheinen
Event-Loop-Architektur       epoll vs io_uring, Single vs Multi-Thread
JadeOS Node-Spec             NodeSpec, MeshPolicy, Discovery, Provisioning