Zum Inhalt

JDL – Generator und Code Emission

Status: Draft Geltungsbereich: Generator-Schicht (Ring 1)


Warum Dieses Dokument Existiert

Nach Parsing, Typprüfung und IR-Konstruktion stellt sich die zentrale Systemfrage: Wie wird validierter, gelowerter IR reproduzierbar zu ausführbaren Artefakten?

Dieses Dokument definiert dafür:

  • die Rolle und Grenzen des Generators,
  • den konkreten Weg von Core-IR zu VM-Protos (Emission-Pipeline),
  • die Struktur der erzeugten Artefakte,
  • das Backend-Modell für VM, FFI-Bridge und optionale Targets.

0. Priorität

Ergänzt:

  • 0003
  • 05-modulsystem-und-ffi.md
  • 05-vm-instruction-set.md
  • 11-jadevalue-und-register-layout.md
  • 13-ir-spec.md

Bei Widerspruch gelten die genannten Kernvorgaben.


1. Rolle des Generators

Der Generator wandelt typisierten, validierten und gelowerten IR sowie bereits materialisierte Descriptoren in Artefakte:

  • VM-Artefakte (Primärpfad),
  • Bridge-Artefakte (FFI/Host),
  • optionale weitere Targets (z. B. WASM).

Nicht Aufgabe des Generators:

  • Semantikprüfung,
  • Effektprüfung,
  • Memory-Safety-Prüfung,
  • Lowering von Extended- zu Core-Instruktionen.

Diese Garantien müssen vorher abgeschlossen sein. Der Generator ist ein read-only Konsument des IR und der CompilerDB-Descriptoren. Er verändert keine Type-, Dispatch-, Effect-, FFI- oder Runtime-Zustände. Seine eigenen Ausgaben können als Artefakte materialisiert werden.


1.2 CompilerDB-Provider-Rolle

Der Generator kann eine GenerateVmModule- oder GenerateBridgeArtifact-Query beantworten. In diesem Fall ist er ein ArtifactProvider der CompilerDB, nicht ein freischwebender Seiteneffekt am Ende der Pipeline. Das Ergebnis wird als VmModuleArtifact, Proto oder Bridge-Artefakt materialisiert und kann einen ArtifactHash erhalten.

Der Generator darf dabei weiterhin nur lesen: Type-, Layout-, Dispatch-, Effect- und FFI-Descriptoren sind Eingaben. Wenn ein benötigter Descriptor fehlt, ist das ein Fehler der vorherigen Phase, kein Anlass für den Generator, Semantik nebenbei nachzuerfinden. Maschinen mögen klare Arbeitsteilung. Menschen offenbar weniger.

2. Pipeline-Vertrag

2.1 Eingabe

LoweredModule enthält:

  • ein IrModule (gemäß 13-ir-spec.md) in dem ausschließlich Core-Instruktionen vorhanden sind (Invariante ExtendedEliminated),
  • aufgelöste Typ-/Layoutmetadaten (via CompilerDb-Queries),
  • extrahierte ServiceInterfaceSpec-Einträge.
// spekulativ

type LoweredModule: struct {
    ir:        IrModule                // Core-only IR nach Lowering
    services:  [ServiceInterfaceSpec]  // extrahierte FFI-/Service-Specs
}

2.2 Kontext

type GeneratorContext: struct {
    typeEngine: TypeQueryApi
    memLayouts: MemLayoutQueryApi
    effects:    EffectQueryApi
}

Invariante: read-only Zugriff auf fremde Subsysteme. Der Generator fragt die Type Engine nach Typgrößen, Alignments, Feld-Offsets und Memory-Policies, verändert aber keine Typdaten.


3. Backend-Modell

3.1 Arten und Artefakte

type BackendKind: enum = | Vm | DBridge | Wasm
type BackendArtifact: enum =
    | VmArtifact(VmModuleArtifact)
    | DBridgeArtifact
    | WasmArtifact

3.2 Protokolle

protocol GeneratorBackend {
    def name(self) -> str
    def kind(self) -> BackendKind
}

protocol ModuleBackend: GeneratorBackend {
    def generateModule(self, module: LoweredModule, ctx: GeneratorContext, cfg: BackendConfig)
        -> Result[BackendArtifact, GeneratorError]
}

protocol BridgeBackend: GeneratorBackend {
    def generateBridge(self, services: [ServiceInterfaceSpec], ctx: GeneratorContext, cfg: BackendConfig)
        -> Result[BackendArtifact, GeneratorError]
}

3.3 Registry

def registerBackend(reg: BackendRegistry, backend: GeneratorBackend) -> BackendRegistry
def getBackend(reg: BackendRegistry, id: BackendId) -> Option[GeneratorBackend]

4. VM-Backend: Emission-Pipeline

Dieser Abschnitt beschreibt den konkreten Weg von LoweredModule zu VmModuleArtifact. Das VM-Backend ist der Referenzpfad für Laufzeitsemantik.

4.1 Übersicht

Der Generator verarbeitet jede IrFunction im LoweredModule einzeln und erzeugt daraus einen Proto. Anschließend werden alle Protos mit Modul-Metadaten zu einem VmModuleArtifact zusammengebaut.

LoweredModule
  │  pro IrFunction:
  ├── 1. Liveness-Analyse
  ├── 2. Register-Allokation (Linear Scan)
  ├── 3. Block-Argument-Auflösung (Copy Coalescing)
  ├── 4. Register-Layout-Berechnung
  ├── 5. Konstantenpool-Aufbau
  ├── 6. Bytecode-Emission
  ├── 7. Sprungauflösung
  ├── 8. Debug-Info-Übersetzung
  └── 9. Proto-Assembly
      Proto (fertiges Funktionsobjekt)
         │  alle Protos + Modul-Metadaten:
      VmModuleArtifact

4.2 Schritt 1: Liveness-Analyse

Die Liveness-Analyse bestimmt für jeden SSA-Wert (IrValueId) in einer Funktion, in welchem Bereich des Kontrollflusses er "lebendig" ist — also zwischen seiner Definition und seiner letzten Verwendung.

Eingabe: IrFunction mit Blöcken, Instruktionen und Terminatoren. Ausgabe: Liveness-Intervalle pro IrValueId.

Die Analyse berücksichtigt:

  • Block-Parameter als Definitions-Punkte,
  • Instruktions-Results als Definitions-Punkte,
  • Operanden-Referenzen als Verwendungs-Punkte,
  • Terminator-Argumente (Block-Argumente an Sprüngen) als Verwendungs-Punkte,
  • Kontrollfluss-Kanten für Werte die über Block-Grenzen hinweg leben.

4.3 Schritt 2: Register-Allokation (Linear Scan)

Die Register-Allokation weist jedem SSA-Wert ein physisches Register R0..R(n-1) zu. Jade verwendet einen Linear-Scan-Allocator.

Warum Linear Scan:

Linear Scan ist der Standard-Allocator für VM-Compiler. Er bietet einen guten Kompromiss zwischen Allokationsqualität und Kompiliergeschwindigkeit. Aufwändigere Verfahren (Graph-Coloring) sind für eine VM-Plattform unverhältnismäßig — die Constitution fordert "einfache, deterministische Übersetzer" (§9.2).

Verfahren:

  1. SSA-Werte nach Start ihres Liveness-Intervalls sortieren.
  2. Intervalle linear durchlaufen und Register zuweisen.
  3. Bei Überlappung: Spilling ist für v0.1 nicht vorgesehen. Die Anzahl der Register ist nicht hardwarebegrenzt (Register-VM), daher kann der Allocator bei Bedarf neue Register vergeben. regCount wächst entsprechend.

Invariante: Nach der Allokation hat jeder SSA-Wert genau ein physisches Register. Keine zwei gleichzeitig lebendigen Werte teilen ein Register. Register können über verschiedene Liveness-Bereiche hinweg wiederverwendet werden.

4.4 Schritt 3: Block-Argument-Auflösung (Copy Coalescing)

Im IR übergeben Terminatoren Werte als Block-Argumente an Zielblöcke. Im Bytecode existiert dieses Konzept nicht — es gibt nur Register und Sprünge.

Strategie: Copy Coalescing

Der Register-Allocator behandelt Block-Argumente und die korrespondierenden Block-Parameter als Coalescing-Hints: er versucht, beide im selben physischen Register zu platzieren. Wenn das gelingt, ist kein Move am Sprung nötig.

// IR:
  block_a:
      %v1 = loadk 42          : i32
      jmp block_b(%v1)

  block_b(%x: i32):
      // %x verwenden

// Nach Coalescing — %v1 und %x in R0:
  block_a:
      LoadK R0, k0
      Jmp block_b

  block_b:
      // R0 verwenden — kein Move nötig

Wenn Coalescing nicht möglich ist (z.B. weil das Register bereits belegt ist), emittiert der Generator explizite Move-Instruktionen vor dem Sprung:

// Fallback — Move vor dem Sprung:
  block_a:
      LoadK R3, k0
      Move R0, R3          // expliziter Move
      Jmp block_b

  block_b:
      // R0 verwenden

Parallele Semantik: Wenn ein Block mehrere Parameter hat, müssen die Moves logisch parallel ausgeführt werden — ein Argument darf nicht ein anderes überschreiben bevor es gelesen wurde. Der Generator löst dies durch Sequenziierung mit temporären Registern wo nötig, oder durch Zyklen-Erkennung im Move-Graph.

4.5 Schritt 4: Register-Layout-Berechnung

Nach der Register-Allokation kennt der Generator die Menge aller verwendeten Register und ihre Typen. Er fragt die MemLayoutQueryApi der Type Engine für jedes Register:

  • size: Größe des Typs in Bytes,
  • align: Alignment-Anforderung,
  • memory: Policy (Value, Rc, Arena, Pool) → entscheidet ob das Register einen Immediate-Wert oder einen HandleId hält.

Daraus berechnet er das Register-Layout als flachen Byte-Buffer (gemäß 11-jadevalue-und-register-layout.md §4):

// spekulativ

type RegLayoutEntry: struct {
    register: u16       // Registerindex
    type:     TypeId    // Typ des Wertes in diesem Register
    offset:   u32       // Byte-Offset im Register-Buffer
    size:     u16       // Größe in Bytes
    kind:     RegKind   // Immediate oder Handle
}

type RegKind: enum =
    | Immediate          // memory: Value — direkte Bytes im Buffer
    | Handle             // memory: Rc/Arena/Pool — HandleId im Buffer

Die VM allokiert beim Frame-Aufbau den gesamten Register-Buffer in einem Stück basierend auf diesem Layout.

4.6 Schritt 5: Konstantenpool-Aufbau

Der Generator sammelt alle Konstanten die in einer Funktion referenziert werden und baut daraus den Konstantenpool — ein indiziertes Array von Werten.

Einträge im Konstantenpool:

  • numerische Literale (i64, f64, ...),
  • String-Literale,
  • TypeIds (für NewStruct, Typ-Checks),
  • Proto-Referenzen (für Closure dst, kProto),
  • Feld-Symbole (für GetField, SetField).
// spekulativ

type ConstEntry: enum =
    | IntConst    { value: i64 }
    | FloatConst  { value: f64 }
    | BoolConst   { value: bool }
    | StrConst    { value: str }
    | TypeConst   { id: TypeId }
    | ProtoRef    { index: u32 }       // Index in die Proto-Liste des Moduls
    | FieldConst  { name: SymbolId }

Jede LoadK dst, k-Instruktion im Bytecode referenziert einen Index in diesen Pool. Der Generator dedupliziert identische Konstanten.

4.7 Schritt 6: Bytecode-Emission

Der Generator iteriert über die Blöcke und Instruktionen des Core-IR und emittiert für jede IR-Instruktion den entsprechenden Opcode mit physischen Register-Operanden.

Übersetzung:

IR:        %v2 = Add %v0, %v1          : i64
           (SSA-Werte)

Bytecode:  Add R2, R0, R1
           (physische Register, encodiert gemäß VM-Spec §4)

Die Operanden-Typen folgen der VM-Spec (§4.2):

  • Reg(u16) für Register-Operanden,
  • Const(u32) für Konstantenpool-Indizes,
  • Jump(i32) oder BlockId(u32) für Sprungziele (vor Auflösung),
  • Upval(u16) für Upvalue-Indizes bei Closures.

Block-Grenzen im IR entsprechen im Bytecode einfach der linearen Aneinanderreihung der Instruktionen. Der Generator ordnet die Blöcke linear an (Blocklinearisierung) und berechnet die Byte-Offsets.

Blocklinearisierung: Die Blöcke einer Funktion müssen in eine lineare Reihenfolge gebracht werden. Der Generator wählt eine Ordnung die unnötige Sprünge minimiert — typischerweise: Entry-Block zuerst, Fallthrough-Nachfolger direkt dahinter, selten besuchte Blöcke (Error-Pfade) am Ende.

4.8 Schritt 7: Sprungauflösung

Nach der Blocklinearisierung werden die abstrakten Sprungziele (IrBlockId) zu konkreten Byte-Offsets im Bytecode-Stream aufgelöst.

Two-Pass-Verfahren:

  1. Erster Pass: Bytecode emittieren mit Platzhalter-Offsets für Sprünge.
  2. Zweiter Pass: Platzhalter durch berechnete Byte-Offsets ersetzen.

Alternativ kann der Generator die Block-Startadressen vorab berechnen wenn alle Instruktionsgrößen bekannt sind.

Fallthrough-Optimierung: Wenn ein Block mit Jmp target endet und target der nächste Block in der linearen Reihenfolge ist, kann der Sprung eliminiert werden (sofern die VM Fallthrough unterstützt — dies ist Implementationswahl gemäß VM-Spec §7).

4.9 Schritt 8: Debug-Info-Übersetzung

Die IR-Spec definiert eine IrSourceMap als Side-Table IrInstId → SourceLoc auf Funktionsebene. Der Generator übersetzt diese in die VM-Form pc → SourceLoc, wobei pc der Byte-Offset im Bytecode-Stream ist.

// spekulativ

type DebugSideTable: struct {
    entries: [(pc: u32, loc: SourceLoc)]
}

Debug vs. Release:

  • Debug-Build: vollständige Side-Table, jede Instruktion hat eine Quellposition. Die VM kann bei Trap eine präzise Fehlermeldung mit Datei, Zeile und Spalte liefern.
  • Release-Build: Side-Table wird gestripped oder auf trap-relevante Stellen reduziert. Der Generator emittiert kompakteren Bytecode (gemäß VM-Spec §5: "Builds dürfen Debug-Infos vollständig strippen").

4.10 Schritt 9: Proto-Assembly

Alle Ergebnisse der vorherigen Schritte werden zum fertigen Proto zusammengebaut:

// spekulativ

type Proto: struct {
    bytecode:       [u8]               // encodierter Core-Instruktionsstream
    constPool:      [ConstEntry]       // Konstantenpool
    regCount:       u16                // Anzahl verwendeter Register
    registerLayout: [RegLayoutEntry]   // Offset + Size + Kind pro Register
    upvalCount:     u16                // Anzahl Upvalues (0 für nicht-Closures)
    argsCount:      u16                // Anzahl Funktionsparameter
    debugInfo:      Option[DebugSideTable]  // pc → SourceLoc (nur Debug)
}

Der Proto ist immutable nach der Erzeugung. Er kann von beliebig vielen Aufrufen (Frames) gleichzeitig geteilt werden.


5. VM-Modul-Artefakt

5.1 Struktur

Das VM-Backend erzeugt pro LoweredModule ein VmModuleArtifact — ein self-contained, ladbares Modul-Paket.

// spekulativ

type VmModuleArtifact: struct {
    name:         ModulePath            // z.B. users::service
    protos:       [Proto]               // alle Funktions-Protos dieses Moduls
    symbolTable:  [ModuleSymbol]        // Name → Proto-Index Zuordnung
    initProto:    Option[u32]           // Index des Init-Protos (für Modul-Initialisierung)
    imports:      [ModuleImport]        // Abhängigkeiten zu anderen Modulen
    version:      ArtifactVersion       // Format-Version für Kompatibilitätsprüfung
}

type ModuleSymbol: struct {
    name:       SymbolId                // Funktionsname
    protoIndex: u32                     // Index in protos[]
    visibility: Visibility              // Pub oder Private
}

type ModuleImport: struct {
    module:  ModulePath                 // importiertes Modul
    symbols: [SymbolId]                 // benötigte Symbole
}

type Visibility: enum = | Pub | Private

5.2 Symboltabelle

Die Symboltabelle bildet Funktionsnamen auf Proto-Indizes ab. Sie ermöglicht:

  • Modul-übergreifende Aufrufe: Wenn Modul A users::service::findUser aufruft, schlägt die VM den Namen in der Symboltabelle von Modul B nach und findet den entsprechenden Proto.
  • Sichtbarkeitsprüfung: Nur Pub-Symbole sind für andere Module auflösbar. Private-Symbole sind Modul-intern.

5.3 Init-Proto

Wenn das IrModule eine initFn enthält (synthetische Initialisierungsfunktion für Modul-Level-Ausdrücke, siehe 13-ir-spec.md §4.2), wird diese als normaler Proto compiliert. initProto referenziert den Index dieses Protos.

Die VM führt den Init-Proto beim Laden des Moduls aus, bevor andere Funktionen des Moduls aufrufbar sind.

5.4 Imports

Die Import-Liste deklariert, welche externen Module und Symbole dieses Modul benötigt. Die VM / der Loader nutzt diese Information um die Lade-Reihenfolge zu bestimmen und fehlende Abhängigkeiten zu melden.


6. Debug- vs. Release-Modus

6.1 Debug-Modus

Im Debug-Modus erzeugt der Generator:

  • vollständige DebugSideTable mit pc → SourceLoc für jede Instruktion,
  • keine Fallthrough-Optimierungen (explizite Sprünge für Debugger-Stepping),
  • keine Superinstruktionen (jeder Core-Opcode einzeln),
  • Register-Poisoning-Hints: der Generator kann Metadaten erzeugen die der VM erlauben, nach Move und Drop den Quell-Slot zu poisonen und Use-after-move/Use-after-drop als Trap zu melden.

6.2 Release-Modus

Im Release-Modus erzeugt der Generator:

  • reduzierte oder keine DebugSideTable,
  • Fallthrough-Optimierungen wo möglich,
  • optionale Superinstruktionen (z.B. GetFieldK als Fusion von LoadK + GetField, gemäß VM-Spec §9.2),
  • Quickening-Vorbereitung: der Generator kann Instruktionen markieren die für Inline-Cache-Spezialisierung geeignet sind (gemäß VM-Spec §10).

6.3 Beobachtungsäquivalenz

Beide Modi müssen beobachtungsäquivalent sein (Constitution §8.2): gleiche Rückgabewerte, gleiche Seiteneffekte, gleiche Fehler. Unterschiede beschränken sich auf Performance und Diagnostik-Tiefe.


7. Bridge Backends

Bridge Backends erzeugen Host-Code aus ServiceInterfaceSpec.

Typischer Output:

  • generierte Hostdateien,
  • kompilierte Shared Libraries.

FFI-Sicherheit wird vor Codegen durch Type Engine entschieden.


8. ServiceInterface als Generator-Eingabe

8.1 Absenkung

ServiceInterface wird in ServiceInterfaceSpec transformiert.

8.2 Kernschema

type ServiceInterfaceSpec: struct {
    name:       str
    link:       str
    convention: CallConv
    functions:  [FfiFunctionSpec]
}

Wichtig:

  • kein implements,
  • Service-Zuordnung erfolgt über provide Service for Handler,
  • ServiceInterface nie direkt in deps.

9. FFI-Sicherheitsregeln

Prüfpunkt:

def checkFfiSafe(ty: TypeId) -> Result[FfiLayout, FfiError]

Verboten in Phase 1 u. a.:

  • Callback-Funktionstypen,
  • captured Closures,
  • unzulässige arena-/share-gebundene Übergänge.

10. Normative Invarianten

  1. Generator arbeitet nur auf validiertem, gelowertem Core-IR (Invariante ExtendedEliminated muss gelten).
  2. Backendzugriffe sind auf Query-APIs beschränkt (read-only).
  3. Register-Allokation verwendet Linear Scan.
  4. Block-Argumente werden via Copy Coalescing aufgelöst; Fallback auf explizite Move-Instruktionen.
  5. Register-Layout wird aus Type-Engine-Metadaten berechnet (MemLayoutQueryApi) und ist compile-time konstant.
  6. VmModuleArtifact ist self-contained: Symboltabelle, Init-Referenz und Import-Deklarationen sind enthalten.
  7. Debug- und Release-Builds müssen beobachtungsäquivalent sein.
  8. ServiceInterfaceSpec enthält kein implements.
  9. FFI-Einstieg benutzt def innerhalb von extern-Blöcken.
  10. Servicebindung nur via provide Service for Handler.
  11. FFI-Unsicherheit führt zu Compile-Fehler vor Codegen.

11. Offene Punkte (Phase 2)

  • Callback-/Trampoline-Modell für FFI-Callbacks,
  • präzise ABI-Policies je Plattform,
  • Stabilitätsverträge für optionale Nicht-VM-Backends,
  • Superinstruktions-Katalog für Release-Modus,
  • Quickening-Strategie und Inline-Cache-Markierung,
  • Calling Convention für Value-Structs > 64 Bit an Funktionsgrenzen (referenziert aus 11-jadevalue-und-register-layout.md),
  • Modul-Linking: Auflösung von ModuleImport-Referenzen zur Ladezeit,
  • Inkrementelle Compilation: Kann der Generator einzelne Protos neu erzeugen ohne das gesamte Modul zu recompilieren?

Änderungsprotokoll

Version 0.2 — Emission-Pipeline, Proto-Struktur und VmModuleArtifact. Konkrete Schritte von LoweredModule zu Proto dokumentiert: Liveness-Analyse, Register-Allokation (Linear Scan), Block-Argument-Auflösung (Copy Coalescing), Register-Layout, Konstantenpool, Bytecode-Emission, Sprungauflösung, Debug-Info-Übersetzung, Proto-Assembly. VmModuleArtifact als strukturiertes Modul-Paket mit Symboltabelle, Init-Proto und Imports definiert. Debug/Release-Unterschiede spezifiziert. Verbindung zu IR-Spec (13) hergestellt. LoweredModule konkretisiert.

Version 0.1 — Initiale Definition. Rolle, Backend-Modell, Pipeline-Vertrag, ServiceInterface, FFI-Regeln.