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:
00–0305-modulsystem-und-ffi.md05-vm-instruction-set.md11-jadevalue-und-register-layout.md13-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 (InvarianteExtendedEliminated), - 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¶
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:
- SSA-Werte nach Start ihres Liveness-Intervalls sortieren.
- Intervalle linear durchlaufen und Register zuweisen.
- 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.
regCountwä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 einenHandleIdhä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)oderBlockId(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:
- Erster Pass: Bytecode emittieren mit Platzhalter-Offsets für Sprünge.
- 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.
Debug vs. Release:
- Debug-Build: vollständige Side-Table, jede Instruktion hat eine
Quellposition. Die VM kann bei
Trapeine 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::findUseraufruft, 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
DebugSideTablemitpc → SourceLocfü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
MoveundDropden Quell-Slot zu poisonen und Use-after-move/Use-after-drop alsTrapzu melden.
6.2 Release-Modus¶
Im Release-Modus erzeugt der Generator:
- reduzierte oder keine
DebugSideTable, - Fallthrough-Optimierungen wo möglich,
- optionale Superinstruktionen (z.B.
GetFieldKals Fusion vonLoadK+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, ServiceInterfacenie direkt indeps.
9. FFI-Sicherheitsregeln¶
Prüfpunkt:
Verboten in Phase 1 u. a.:
- Callback-Funktionstypen,
- captured Closures,
- unzulässige arena-/share-gebundene Übergänge.
10. Normative Invarianten¶
- Generator arbeitet nur auf validiertem, gelowertem Core-IR
(Invariante
ExtendedEliminatedmuss gelten). - Backendzugriffe sind auf Query-APIs beschränkt (read-only).
- Register-Allokation verwendet Linear Scan.
- Block-Argumente werden via Copy Coalescing aufgelöst;
Fallback auf explizite
Move-Instruktionen. - Register-Layout wird aus Type-Engine-Metadaten berechnet
(
MemLayoutQueryApi) und ist compile-time konstant. VmModuleArtifactist self-contained: Symboltabelle, Init-Referenz und Import-Deklarationen sind enthalten.- Debug- und Release-Builds müssen beobachtungsäquivalent sein.
ServiceInterfaceSpecenthält keinimplements.- FFI-Einstieg benutzt
definnerhalb vonextern-Blöcken. - Servicebindung nur via
provide Service for Handler. - 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.