Zum Inhalt

JDL – Intermediate Representation (IR) v0.1

Status: Draft Geltungsbereich: IR-Struktur, IR-Builder-Subsystem, Desugaring, Lowering-Grenze


Warum Dieses Dokument Existiert

Alle Kernspezifikationen – Constitution, VM Instruction Set, Generator, CompilerDb – referenzieren den IR als zentrale Datenstruktur, ohne seine konkrete Form zu definieren. Dieses Dokument schließt diese Lücke:

  • Was ist die Containerstruktur des IR?
  • Wie werden SSA-Werte und Kontrollfluss repräsentiert?
  • Welches Subsystem erzeugt den IR, und was sind seine Eingaben und Ausgaben?
  • Welche JDL-Konstrukte werden wann und wie in IR übersetzt?
  • Wo endet die Verantwortung des IR-Builders und wo beginnt Lowering?

0. Priorität

Ergänzt:

  • 0008
  • 12-design-constitution.md
  • 05-vm-instruction-set.md
  • 06-generator-und-code-emission.md
  • jade-compiler-db-spec.md

Bei Widerspruch gelten die genannten Kernvorgaben. Insbesondere:

  • Die Constitution definiert IR-Invarianten, Pass-Contracts und die Core/Extended-Trennung.
  • Die VM Instruction Set Spec definiert die Core- und Extended-Instruktionen.
  • Die Generator-Spec definiert LoweredModule als Ausgabeformat nach Lowering.
  • Die CompilerDb-Spec definiert den Query-basierten Subsystem-Aufbau.

1. Einordnung in die Pipeline

Source
  → Parser → AST
  → Name Resolution       (CompilerDb Query, auf AST)
  → Type Checking          (CompilerDb Query, auf AST)
  → IR-Builder             (eigenes Subsystem, CompilerDb Query)
      konsumiert: typisierten AST + Typinfo + Effekte
      produziert: IR-Modul mit Extended-Instruktionen
  → Pass-Pipeline          (Optimierungen, Analysen — auf IR)
  → Lowering-Pässe         (Extended → Core, mechanisch)
  → Verifier               (prüft Core-only + alle Invarianten)
  → Generator              (read-only Konsument von LoweredModule)
      → VM-Backend → Bytecode/Wordcode
      → Bridge-Backend → FFI-Artefakte
      → (optional) WASM-Backend
  → VM führt aus

Der AST bleibt über die gesamte Pipeline hinweg immutable. Der IR-Builder liest den AST und die Ergebnisse der Type Engine via CompilerDb-Queries und produziert eine neue, eigenständige Repräsentation.


2. IR-Builder als CompilerDb-Subsystem

2.1 Rolle

Der IR-Builder ist ein eigenständiges Subsystem mit eigenen Query-Handlern in der CompilerDb. Er ist weder Teil des Frontends (das Analyse macht) noch des Generators (der read-only konsumiert).

Seine Verantwortung:

  • Übersetzung von JDL-Semantik in IR-Strukturen,
  • Auflösung aller syntaktischen und semantischen Zucker (siehe §6),
  • Emission von Extended-Instruktionen für hochlevellige Konstrukte,
  • Erzeugung expliziter Kontrollfluss-Strukturen (Basic Blocks, Terminatoren).

Nicht seine Verantwortung:

  • Typprüfung (abgeschlossen vor dem IR-Builder),
  • Semantikprüfung und Effektprüfung (abgeschlossen vor dem IR-Builder),
  • Lowering von Extended zu Core (spätere Pipeline-Phase),
  • Register-Allokation und Layout-Berechnung (Generator-Aufgabe).

2.2 CompilerDb-Integration

Der IR-Builder registriert Query-Handler in der CompilerDb:

// spekulativ — Query-Definitionen

// Baut das IR für ein gesamtes Modul.
// Abhängigkeiten: CheckModule, GetFileAST, Type-Engine-Queries
type BuildModuleIR: struct {
    moduleId: ModuleId
    // → IrModule
}

// Baut den IR für eine einzelne Funktion.
// Abhängigkeiten: GetFunctionAST, ResolveType, CheckEffects
type BuildFunctionIR: struct {
    functionId: FnId
    // → IrFunction
}

2.3 Arena-Zuordnung

Der IR-Builder besitzt eine eigene Arena gemäß CompilerDb-Architektur. IR-Datenstrukturen werden in dieser Arena allokiert. Die Arena wird zurückgesetzt, wenn die IR-Zwischendarstellung nicht mehr benötigt wird (nach Generator-Emission).


3. SSA-Modell: Block-Argumente

3.1 Entscheidung

Der IR verwendet Block-Argumente statt Phi-Nodes für die Zusammenführung von SSA-Werten an Kontrollfluss-Merge-Punkten.

3.2 Begründung

Block-Argumente reduzieren die Anzahl der Konzepte im IR:

  • Kein spezielles Phi-Pseudo-Instruktionskonstrukt nötig.
  • Ein Block ist einheitlich: Parameter + Body + Terminator.
  • Terminatoren übergeben Werte explizit an Zielblöcke — die Datenfluss-Kante ist am Sprung sichtbar, nicht als implizite Rückreferenz im Zielblock.
  • Pass-Transformationen (Block-Splitting, Kanten-Umleitung) sind einfacher, weil nur die Sprungargumente angepasst werden müssen.

Dieses Modell folgt MLIR, Cranelift und Swift SIL.

3.3 Semantik

Wenn ein Block Parameter deklariert, muss jeder Terminator der zu diesem Block springt die korrekte Anzahl und Typen von Argumenten übergeben. Die Argumente werden ausgewertet, bevor der Zielblock beginnt — das garantiert die korrekte parallele Semantik bei mehreren Parametern.

Beispiel:

// spekulativ — IR-Textdarstellung

  block_then:
      %v1 = loadk 1                     : i32
      jmp block_merge(%v1)

  block_else:
      %v2 = loadk 2                     : i32
      jmp block_merge(%v2)

  block_merge(%x: i32):                 // Block-Argument
      // %x ist %v1 oder %v2, je nach Herkunft

4. Containerstruktur

4.1 Übersicht

IrModule
  ├── Metadaten (Name, Source, Abhängigkeiten)
  ├── Typ-Definitionen
  ├── Modul-Konstanten
  ├── Modul-Initialisierungsfunktion (synthetisch)
  └── Funktionen
        └── IrFunction
              ├── Signatur (Parameter, Return-Typ, Effekte)
              ├── Entry-Block
              └── Blöcke
                    └── IrBlock
                          ├── Block-Parameter (SSA-Werte)
                          ├── Body (geordnete Instruktionsliste)
                          └── Terminator

4.2 IrModule

Ein IR-Modul entspricht einer JDL-Quelldatei. Es ist die Einheit die der IR-Builder als Query-Ergebnis produziert.

// spekulativ

type IrModule: struct {
    name:        ModulePath          // z.B. users::service
    functions:   [IrFunction]        // alle Funktionen inkl. synthetischer
    types:       [TypeDefRef]        // Typ-Definitionen dieses Moduls (Verweis in Type Engine)
    constants:   [IrConstant]        // Modul-Level-Konstanten (compile-time auswertbar)
    initFn:      Option[IrFunction]  // synthetische Init-Funktion für Modul-Level-Ausdrücke
    source:      SourceFileRef       // Rückverweis auf Quelldatei
}

Modul-Initialisierung: JDL erlaubt Modul-Level-Ausdrücke wie val counter: Atomic[u64] = Atomic.new(0u64). Da solche Ausdrücke keine Compile-Zeit-Konstanten sind, erzeugt der IR-Builder eine synthetische Initialisierungsfunktion (__init), die beim Modul-Load ausgeführt wird. Reine Compile-Zeit-Konstanten landen in constants.

4.3 IrFunction

// spekulativ

type IrFunction: struct {
    name:       SymbolId             // Funktionsname (oder synthetischer Name)
    params:     [IrParam]            // Funktionsparameter
    returnType: TypeId               // Rückgabetyp
    effects:    EffectSet            // deklarierte Effekte
    blocks:     [IrBlock]            // blocks[0] ist immer der Entry-Block
    sourceMap:  IrSourceMap          // Side-Table: IrInstId → SourceLoc
}

type IrParam: struct {
    name:  SymbolId
    type:  TypeId
    value: IrValueId                 // der SSA-Wert den dieser Parameter definiert
}

SourceMap als Side-Table: Debug-Informationen werden nicht inline auf einzelnen Instruktionen gespeichert, sondern als separate Zuordnungstabelle IrInstId → SourceLoc auf Funktionsebene. Das folgt dem Prinzip der VM Instruction Set Spec (§5: „Debug-Infos werden als Side-Table gespeichert") und hält die Instruktionsdatenstruktur kompakt. Pässe die Instruktionen transformieren müssen die SourceMap entsprechend aktualisieren.

4.4 IrBlock

// spekulativ

type IrBlockId: struct { id: u32 }

type IrBlock: struct {
    id:         IrBlockId
    params:     [IrBlockParam]       // Block-Argumente (SSA, siehe §3)
    body:       [IrInst]             // geordnete Instruktionsliste
    terminator: IrTerminator         // genau einer, immer am Ende
}

type IrBlockParam: struct {
    value: IrValueId                 // der SSA-Wert den dieser Parameter definiert
    type:  TypeId
}

Geordnete Instruktionsliste: Der Body ist eine geordnete Liste, keine Graph-Repräsentation (Sea-of-Nodes). Reine Instruktionen könnten theoretisch in beliebiger Reihenfolge stehen, solange Datenabhängigkeiten respektiert werden, aber eine lineare Liste ist einfacher zu debuggen, zu serialisieren und zu testen. Das ist ein bewusster Tradeoff zugunsten von Verständlichkeit (Constitution §0).

4.5 IrInst

// spekulativ

type IrValueId: struct { id: u32 }
type IrInstId:  struct { id: u32 }

type IrInst: struct {
    id:       IrInstId               // eindeutige ID für SourceMap-Referenz
    result:   Option[IrValueId]      // None für Instruktionen ohne Ergebnis (z.B. SetField)
    op:       IrOp                   // Opcode (Core oder Extended)
    operands: [IrOperand]            // Eingabewerte
    type:     TypeId                 // Ergebnistyp (redundant, aber vereinfacht Verifier/Pässe)
}

TypeId auf Instruktionen: Der Ergebnistyp ist technisch aus den Operanden ableitbar, wird aber explizit gespeichert. Das vermeidet wiederholt Typ-Queries in Pässen und Verifier, auf Kosten von Speicher. In einem Arena-basierten System ist dieser Tradeoff akzeptabel.

4.6 IrOperand

// spekulativ

type IrOperand: enum =
    | Value     { id: IrValueId }     // Referenz auf einen SSA-Wert
    | Const     { ref: ConstRef }     // Referenz in den Konstantenpool
    | TypeRef   { id: TypeId }        // Typreferenz (z.B. für NewStruct, CastTo)
    | FieldRef  { name: SymbolId }    // Feldname (z.B. für GetField, SetField)
    | BlockRef  { id: IrBlockId }     // Blockreferenz (nur in Terminatoren)

4.7 IrTerminator

Terminatoren beenden einen Block und definieren den Kontrollfluss. Sie sind strukturell von normalen Instruktionen getrennt, damit jeder Block garantiert genau einen hat.

// spekulativ

type IrTerminator: enum =
    | Jmp {
        target: IrBlockId
        args:   [IrValueId]          // Block-Argumente für den Zielblock
      }
    | JmpIf {
        cond:      IrValueId
        thenBlock: IrBlockId
        thenArgs:  [IrValueId]
        elseBlock: IrBlockId
        elseArgs:  [IrValueId]
      }
    | Return {
        value: IrValueId
      }
    | Trap {
        reason: TrapReason
      }
    | Halt

4.8 IrOp — Core und Extended

IrOp bildet die Core/Extended-Trennung der VM Instruction Set Spec ab. Im IR dürfen beide Kategorien koexistieren. Nach dem Lowering dürfen nur noch Core-Operationen vorhanden sein.

// spekulativ

type IrOp: enum =
    // === Core (VM-Execution-Level) ===
    | LoadK                           // Konstante laden
    | LoadBool                        // Bool-Immediate
    | Add | Sub | Mul | Div | Mod     // Arithmetik
    | Neg                             // Unäre Negation
    | Eq | Lt                         // Vergleich (Core-Minimum)
    | Not                             // Logische Negation
    | Call                            // Funktionsaufruf
    | Closure                         // Closure erzeugen
    | LoadUp | StoreUp | CloseUp      // Upvalue-Operationen
    | NewStruct                       // Struct allokieren
    | GetField | SetField             // Feldzugriff
    | NewArray                        // Array allokieren
    | GetIndex | SetIndex             // Indexzugriff
    | Move                            // SSA-Wert verschieben, Quelle invalidieren
    | Drop                            // End-of-Lifetime signalisieren

    // === Extended (vor Lowering eliminiert) ===
    | TailCall                        // → Call + Return
    | Ne | Le | Gt | Ge               // → Eq/Lt/Not-Kombinationen
    | LoadNull | LoadUnit             // → LoadK mit spezieller Konstante
    | LoadI8 | LoadI16 | LoadI32      // → LoadK mit Immediate
    | NewStructLiteral                // Struct mit Feldzuweisungen in einer Op
    | Match                           // Pattern-Match-Konstrukt
    | ShortCircuitAnd                 // → JmpIf-Kette
    | ShortCircuitOr                  // → JmpIf-Kette
    | CollectToArray                  // |> [] Sugar
    | CollectToMap                    // |> {} Sugar
    | WithScope                       // with-Block (acquire/release)
    | Interpolate                     // f"..." String-Interpolation

Die Liste ist nicht abschließend. Plugins dürfen Extended erweitern (Constitution §3.3), aber nicht den Core.


5. Closures im IR

5.1 Problem

Closures capturen Werte aus umgebenden Scopes. Im IR müssen diese Captures explizit dargestellt werden, da SSA-Werte grundsätzlich nur innerhalb ihrer definierenden Funktion sichtbar sind.

5.2 Darstellung

Der IR-Builder erzeugt Closures als eigene IrFunction mit einer expliziten Capture-Liste. Die Closure-Erzeugung im umgebenden Kontext wird als Closure-Instruktion emittiert, die die zu capturenden SSA-Werte referenziert.

// spekulativ

type IrCapture: struct {
    outerValue: IrValueId            // der SSA-Wert im umgebenden Scope
    innerParam: IrValueId            // der SSA-Wert innerhalb der Closure
    type:       TypeId
    mode:       CaptureMode          // ByValue oder ByRef
}

type CaptureMode: enum =
    | ByValue                        // Wert wird kopiert
    | ByRef                          // Referenz auf den Wert (Upvalue)

Im umgebenden Kontext:

// spekulativ
%fn    = closure @lambda_0(%captured1, %captured2)  : ClosureType

In der Closure-Funktion selbst werden Captures als Upvalue-Loads im Entry-Block verfügbar gemacht.


6. Desugaring-Katalog

Der IR-Builder löst alle JDL-Zucker auf. Der AST bleibt dabei immutable. Die folgenden Tabellen dokumentieren jedes Desugaring, die benötigte Typinformation und das resultierende IR-Konstrukt.

6.1 Kategorie 0 — Compile-Time-Eliminierung (vor IR-Builder)

Diese Transformationen passieren im Frontend, bevor der IR-Builder läuft.

Konstrukt Transformation Phase
@cfg(predicate) AST-Pruning gegen Build-Kontext Nach Parse, vor Name Resolution
@assert(pred, msg?) Compile-Error wenn Predicate false Nach Parse (early) oder nach Typecheck (late)

6.2 Kategorie 1 — Syntaktisch (keine Typinfo nötig, trotzdem im IR-Builder)

Diese Transformationen könnten theoretisch auf dem AST laufen, werden aber der Einfachheit halber im IR-Builder erledigt. Das hält den AST immutable und vermeidet einen separaten Rewrite-Pass.

Konstrukt JDL IR Anmerkung
Short-Circuit And a and b JmpIf auf a, dann b oder false VM-Spec: Extended, über JmpIf/Jmp
Short-Circuit Or a or b JmpIf auf a, dann true oder b VM-Spec: Extended, über JmpIf/Jmp
Pipeline a \|> f call @f(%a) Rein syntaktische Umordnung
Pipeline mit Args a \|> f(x) call @f(%a, %x) Rein syntaktische Umordnung
String-Interpolation f"Hallo {name}" Extended Interpolate oder call @str.concat Kette Kann als Extended erhalten bleiben

6.3 Kategorie 2 — Semantisch (benötigt Typinfo, muss im IR-Builder geschehen)

Konstrukt JDL IR Benötigte Typinfo
Result-Propagation val x =? f() call + isErr + JmpIf → Error-Pfad mit CastTo, Ok-Pfad mit unwrapOk Return-Typ der Fn, CastTo-Instanz
Optional Chaining x?.field match auf Option: SomeGetField + Some, NoneNone Typ ist Option[T], Feldtyp
Nil-Coalescing x ?? default match auf Option: Some(v)v, Nonedefault Typ des Option
with-Block with expr as name { body } call @acquire + Body-Blöcke + call @release (auch bei Error-Pfaden) Scopeable[R]-Instanz
Scope-Cleanup implizites Drop am Scope-Ende Drop-Instruktionen für Disposable-Werte Drop-Policy des Typs
Operator-Aufruf a + b call @Add.add(%a, %b) Protocol-Instanz Add für Typ von a
Span-Operator a..b call @Spannable.span(%a, %b) Spannable[T]-Instanz
Implizite Konversion kontextabhängig call @CastTo.castTo(%x) CastTo[T]: Lossless-Instanz
Pattern Match match expr { \| Arm => ... } Extended Match oder Baum aus Eq/JmpIf + Destructuring Typ des Scrutinee, Enum-Varianten
Collect-Sugar \|> [] Extended CollectToArray oder call @Collect.collect Collect-Instanz
Map-Collect \|> {} Extended CollectToMap oder call @Collect.collect Collect-Instanz
Default-Parameter f(x) mit fehlendem Arg Fehlende Args werden mit Default-Werten eingesetzt Default-Wert aus Signatur
Protocol als Param def f(g: Generator[T]) Monomorphisierung: konkreter Typ statt Protocol Concrete-Type-Resolution
Derive :> Derive([Add]) Synthetische IrFunction für generierte provide-Blöcke Struct-Felder, Protocol-Anforderungen

7. Lowering: Extended → Core

7.1 Abgrenzung zum IR-Builder

Phase Verantwortung Art der Arbeit
IR-Builder JDL-Semantik → IR Semantische Übersetzung. Braucht Typinfo, Effekte, Protocol-Instanzen. Emittiert Extended.
Lowering Extended → Core Mechanische Transformation. Braucht keine JDL-Semantik, nur die Kenntnis wie eine Extended-Op in Core zerfällt.

7.2 Lowering-Patterns

Die folgende Liste definiert, wie Extended-Instruktionen zu Core-Sequenzen transformiert werden. Jedes Pattern ist eine rein mechanische Ersetzung.

Arithmetik/Vergleich:

Extended Core-Expansion
Ne dst, a, b Eq tmp, a, bNot dst, tmp
Le dst, a, b Lt tmp, b, aNot dst, tmp
Gt dst, a, b Lt dst, b, a
Ge dst, a, b Lt tmp, a, bNot dst, tmp

Kontrollfluss:

Extended Core-Expansion
TailCall dst, func, args Call dst, func, argsReturn dst (oder VM-Optimierung)
ShortCircuitAnd a, b → dst JmpIf a, L_eval_b else dst = false; Jmp L_end / L_eval_b: dst = b / L_end
ShortCircuitOr a, b → dst JmpIf a, L_true else dst = b; Jmp L_end / L_true: dst = true / L_end

Aggregates:

Extended Core-Expansion
NewStructLiteral T { f1: v1, f2: v2 } NewStruct dst, TSetField dst, f1, v1SetField dst, f2, v2

Cancellation-aware Scheduling / Messaging:

Konstrukt Lowering-Hinweis
task.await() normaler Call auf den Stdlib-Wrapper, der intern jade::vm::await(Some(handle)) aufruft und das rohe AwaitOutcome auf TaskOutcome[T] abbildet
yield() normaler Call auf den Stdlib-Wrapper, der intern jade::vm::await(None) aufruft und Yielded bzw. Cancelled behandelt
receiver.recv() normaler Call auf den Stdlib-Wrapper, der intern jade::vm::receive() aufruft und rohe VM-Outcomes (Message / Cancelled) in Channel-Semantik (Message / Closed / Cancelled) ueberfuehrt

Normativ: Cancellation fuehrt keine neue Core-Instruktion ein. Scheduling-/Messaging-Cancellation bleibt eine Eigenschaft der Runtime-Intrinsics und ihrer Stdlib-Wrapper. Das respektiert die Constitution (Core klein halten, High-Level-Politik nicht in die VM verlagern).

Pattern Match (informativ):

Match auf einem Enum mit N Varianten kann gesenkt werden zu:

  • einer Kette von Eq + JmpIf für kleine N,
  • einer Jump-Table-Instruktion für große N (falls im Core vorhanden),
  • einem Baum aus binären Vergleichen für sortierbare Werte.

Die Wahl der Strategie obliegt dem Lowering-Pass und kann heuristisch oder profilgesteuert erfolgen.

7.3 Invariante nach Lowering

Nach Abschluss aller Lowering-Pässe gilt:

  • Kein IrInst enthält einen IrOp aus der Extended-Kategorie.
  • Alle Block-Argumente sind korrekt (Typ und Anzahl stimmen an jedem Sprung).
  • Alle SSA-Invarianten gelten (jeder Wert wird genau einmal definiert).

Der Verifier prüft diese Invarianten vor der Übergabe an den Generator.


8. Beziehung zu Typen, Effekten und Policies

8.1 Grundprinzip

Typ-, Effekt- und Policy-Informationen werden nicht redundant im IR dupliziert. Der IR referenziert sie über IDs in die Type Engine der CompilerDb.

  • TypeId verweist auf vollständige Typinformation in der Type Engine.
  • EffectSet ist eine Menge von Effekt-Deklarationen auf Funktionsebene.
  • Policies (Memory, Own, Share, etc.) sind Bestandteil des TypeId und werden über Type-Engine-Queries aufgelöst.

8.2 Konsequenz für Pässe

Pässe die Typinformation benötigen (z.B. für typabhängige Optimierungen) fragen die CompilerDb. Da Typ-Queries gecacht sind, ist der Overhead minimal. Der IR selbst bleibt schlank.

Ausnahme: Der Ergebnistyp (TypeId) auf jeder IrInst ist bewusst redundant gespeichert (siehe §4.5), um den häufigsten Query-Fall zu vermeiden.


9. IR-Textdarstellung (informativ)

Die IR hat keine normative Textform. Für Debugging, Tests und Dokumentation wird die folgende informelle Notation verwendet:

fn @functionName(%param0: Type0, %param1: Type1) -> ReturnType
    effects: [Effect1, Effect2]
{
  entry:
      %v0 = loadk 42                          : i32
      %v1 = call @someFunction(%param0, %v0)   : str
      jmpif %cond, then_block(%v1), else_block()

  then_block(%x: str):
      %v2 = getfield %x, name                 : str
      jmp merge(%v2)

  else_block:
      %v3 = loadk "default"                   : str
      jmp merge(%v3)

  merge(%result: str):
      return %result
}

Konventionen:

  • %name kennzeichnet SSA-Werte.
  • @name kennzeichnet globale Symbole (Funktionen, Typen).
  • : Type nach einer Instruktion ist der Ergebnistyp.
  • Terminatoren stehen am Ende jedes Blocks, ohne %result =.
  • Block-Argumente stehen in Klammern nach dem Blocknamen.
  • Die Textform ist kein Bestandteil des JDL-Sprachstandards.

10. Vollständiges Beispiel

10.1 JDL-Quellcode

Aus Spec 03 — registerUser (vereinfacht):

def 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)
}

10.2 IR-Dump (spekulativ)

fn @registerUser(%name: str, %email: str, %age: i32, %role: Role)
    -> Result[UserResponse, AppError]
    effects: [UserDb, LogService]
{

  entry:
      // =? desugared: validateEmail aufrufen, Result prüfen
      %result0 = call @validateEmail(%email)              : Result[str, UserError]
      %is_err0 = call @Result.isErr(%result0)             : bool
      jmpif %is_err0, err_prop0(), ok0()

  ok0:
      %validEmail = call @Result.unwrapOk(%result0)       : str

      // Struct-Literal (Extended: NewStructLiteral)
      %now        = call @Instant.now()                    : Instant
      %millis     = call @Instant.millis(%now)             : i64
      %millis_str = call @i64.toString(%millis)            : str
      %id_inner   = interpolate "u-", %millis_str          : str
      %userId     = newstructliteral UserId { value: %id_inner }  : UserId
      %user       = newstructliteral User {
                        id: %userId, name: %name,
                        email: %validEmail, age: %age, role: %role
                    }                                      : User

      // =? desugared: UserDb.insertUser aufrufen
      %result1 = call @UserDb.insertUser(%user)            : Result[User, UserError]
      %is_err1 = call @Result.isErr(%result1)              : bool
      jmpif %is_err1, err_prop1(), ok1()

  err_prop0:
      // =? Error-Pfad: UserError → AppError via CastTo
      %err0       = call @Result.unwrapErr(%result0)       : UserError
      %appErr0    = call @CastTo.castTo[AppError](%err0)   : AppError
      %errResult0 = call @Result.Err(%appErr0)             : Result[UserResponse, AppError]
      return %errResult0

  err_prop1:
      %err1       = call @Result.unwrapErr(%result1)       : UserError
      %appErr1    = call @CastTo.castTo[AppError](%err1)   : AppError
      %errResult1 = call @Result.Err(%appErr1)             : Result[UserResponse, AppError]
      return %errResult1

  ok1:
      %saved = call @Result.unwrapOk(%result1)             : User

      // String-Interpolation (Extended: Interpolate)
      %sName   = getfield %saved, name                     : str
      %sEmail  = getfield %saved, email                    : str
      %logMsg  = interpolate "Registriert: ", %sName, " <", %sEmail, ">"  : str
      %_log0   = call @LogService.info(%logMsg)            : ()

      // Pattern Match (Extended: Match)
      %sRole   = getfield %saved, role                     : Role
      match %sRole {
          Role.Admin => match_admin()
          _          => match_done()
      }

  match_admin:
      %warnMsg = interpolate "Admin erstellt: ", %sName    : str
      %_log1   = call @LogService.warn(%warnMsg)           : ()
      jmp match_done()

  match_done:
      // Implizite Konversion: CastTo[UserResponse] for User
      %response  = call @CastTo.castTo[UserResponse](%saved)  : UserResponse
      %okResult  = call @Result.Ok(%response)              : Result[UserResponse, AppError]
      return %okResult
}

10.3 Beobachtungen

  • =? ist zu explizitem Kontrollfluss mit Error-Pfad und CastTo-Aufruf aufgelöst.
  • Struct-Literale verwenden die Extended-Instruktion NewStructLiteral, die im Lowering zu NewStruct + N × SetField expandiert wird.
  • String-Interpolation verwendet die Extended-Instruktion Interpolate.
  • match verwendet die Extended-Instruktion Match, die im Lowering zu Eq/JmpIf-Ketten expandiert wird.
  • Implizite Konversionen sind explizite CastTo-Aufrufe — keine Magie.
  • Service-Calls sind normale call-Instruktionen; der Dispatch-Mechanismus ist Sache der VM/Runtime, nicht des IR.
  • Block-Argumente kommen in diesem Beispiel nicht vor, da kein Kontrollfluss-Merge Werte zusammenführen muss.

11. IR-Invarianten

Die Constitution (§4.2) definiert IR-Invarianten. Dieses Dokument ergänzt ihre Bedeutung im Kontext der konkreten IR-Struktur:

Invariante Bedeutung für die IR-Struktur
SSAForm Jeder IrValueId wird genau einmal definiert (als Block-Parameter oder Instruktions-Result). Verwendungen referenzieren existierende Definitionen.
DominatorsValid Der CFG (definiert durch Terminatoren) hat einen gültigen Dominator-Baum.
TypesResolved Jeder TypeId im IR verweist auf einen vollständig aufgelösten Typ in der Type Engine.
PoliciesConsistent Policies (via TypeId) widersprechen sich nicht. Z.B. kein Move auf share: Sync-Werte.
EffectsNormalized Effekte in EffectSet sind in normalisierter Form.
NoDeadBlocks Jeder Block ist vom Entry-Block aus erreichbar.
ExtendedEliminated Nach Lowering: kein IrOp aus der Extended-Kategorie mehr vorhanden.

ExtendedEliminated ist eine neue Invariante die der Verifier nach dem Lowering prüft, bevor der Generator das IR konsumiert.


12. Offene Punkte

12.1 Closure-Capture-Analyse

Die genaue Strategie für die Closure-Capture-Analyse (ByValue vs. ByRef, Upvalue-Flattening, Capture-Optimierungen) ist noch nicht spezifiziert. §5 definiert die Datenstruktur, nicht die Analysestrategie.

12.2 IR-Textform

Die informelle Notation in §9 ist weder normativ noch stabil. Eine formale, parsebare Textform wäre für Tests und Debugging wertvoll, ist aber kein MVP-Blocker. Die Constitution erlaubt das als spätere Ergänzung (§1.2: IR ist zunächst ein Implementierungsdetail).

12.3 Generics und Monomorphisierung

Wie generische Funktionen im IR dargestellt werden — als generisches Template das bei Instanziierung kopiert wird, oder als bereits monomorphisierte Instanzen — ist noch offen. Die Entscheidung hängt von der Monomorphisierungs-Strategie der Type Engine ab.

12.4 CallGraph und andere deklarative Runtime-Konstrukte

CallGraph, Actor-Patterns und weitere deklarative Runtime-Konstrukte (Spec 01, 03, 07) erzeugen komplexere IR-Strukturen (Node-Graphen, Receive-Loops, eventuelle Scheduling-/Reactive-Baeume). Deren genaue IR-Darstellung ist noch nicht spezifiziert.

Bereits entschieden: Auch diese Konstrukte muessen Scheduling- und Messaging-Cancellation ueber dieselben Runtime-Intrinsics modellieren; sie erhalten keine eigene, abweichende Cancellation-Welt im IR.

12.5 IrOp-Vollständigkeit

Die IrOp-Enumeration in §4.8 ist nicht abschließend. Neue Extended-Operationen können hinzukommen, insbesondere für Plugins (Constitution §3.3) und zukünftige Sprachfeatures.


Änderungsprotokoll

Version 0.1 — Initiale Definition. Containerstruktur, SSA-Modell mit Block-Argumenten, IR-Builder als CompilerDb-Subsystem, Desugaring-Katalog, Lowering-Grenze, Closure-Darstellung, Invarianten.