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 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. Er verändert keine Subsystem-Zustände.
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.