Zum Inhalt

JDL — Compiler-Annotationen (@...)

Status: Draft
Geltungsbereich: Frontend, Compiler-Pipeline, Tooling, Runtime-Bindung
Abhängigkeiten: 00-refinements.md, 13-ir-spec.md, jade-compiler-db-spec.md, 08-intrinsics-vm-api.md
Nicht-Ziele: User-Makros, allgemeines Metaprogramming, Runtime-Code-Rewriting


0. Zweck, Scope und Nicht-Ziele

Dieses Dokument definiert die Compiler-Annotationen von JDL.
Compiler-Annotationen sind eine kleine, deterministische Sonderoberfläche zwischen JDL und Jade. Sie existieren ausschließlich für Dinge, die:

  • der Compiler zwingend wissen muss,
  • deterministische Build- oder Runtime-Verträge ausdrücken,
  • statische Registrierung für Tooling oder Runtime-Hooks bereitstellen,
  • oder frühe Compile-Time-Entscheidungen ermöglichen, die nicht natürlich in das normale Typ- und Refinement-System fallen.

Compiler-Annotationen sind nicht:

  • ein allgemeines Makro-System,
  • eine zweite Sprachebene,
  • ein Ersatz für das Typ-Metasystem,
  • ein Mittel zur Laufzeit-Umschreibung von Programmen.

Alles domänenspezifische Meta gehört in:

  • Meta-Records (<{ ... }>),
  • Refinements (:>),
  • TypeFns,
  • Protocols / provide,
  • Generators / Collectors,
  • oder normale JDL-Abstraktionen.

1. Grundbegriffe

1.1 Compiler-Annotation

Eine Compiler-Annotation ist ein mit @ eingeleiteter, compilerseitig bekannter Marker oder Vertrag mit fest definierter Semantik.

Beispiele:

@cfg(target.os == "linux")
@test
@deprecated("Use newApi() instead")
@lang_item("main")

1.2 Pruning

Pruning ist das vollständige Entfernen von AST-Knoten vor Name Resolution, Type Checking und IR-Building.

1.3 Validation

Validation ist eine Compile-Time-Prüfung, die bei Verletzung einen Compilerfehler erzeugt, aber keinen Code umschreibt.

1.4 Diagnose-Annotation

Eine Annotation, deren einzige Wirkung diagnostisch ist, z. B. Warnungen bei Benutzung eines Symbols.

1.5 Registry-Annotation

Eine Annotation, die Symbole oder Funktionen für Tooling, Testsysteme oder Runtime-Bindung deterministisch registrierbar macht.

1.6 Runtime-Hook

Ein Symbol mit spezieller, vom Compiler oder der Runtime erwarteter Rolle, z. B. main oder ein Panic-Handler.

1.7 Build-Kontext

Der Build-Kontext ist die feste Menge compile-time bekannter Fakten, gegen die @cfg und frühe @assert-Prädikate ausgewertet werden.

Beispiele:

  • target.os
  • target.arch
  • build.mode
  • feature("ffi")

Eine Link-Unit ist die Menge von Modulen / Artefakten, die gemeinsam zu einem ausführbaren Programm oder einer Bibliothek verbunden werden.


2. Allgemeine Invarianten

Die folgenden Regeln gelten für alle Compiler-Annotationen.

2.1 Determinismus

Compiler-Annotationen müssen deterministisch auswertbar sein.

Gleicher Quelltext + gleicher Build-Kontext = gleiche Annotationsergebnisse.

2.2 Keine Laufzeit-Umschreibung

Compiler-Annotationen führen keine dynamische Laufzeit-Umschreibung ein. Sie erzeugen keine „Meta-Logik“, die sich erst bei Programmausführung entfaltet.

2.3 Keine zweite Sprache

Compiler-Annotationen etablieren keine zweite, frei programmierbare Sprache innerhalb von JDL.

Es gibt:

  • keine user-definierten Compiler-Annotationen mit eigener Semantik,
  • kein allgemeines Makro-System,
  • kein unkontrolliertes AST-Rewriting durch Benutzer.

2.4 Typ-Meta hat Vorrang für Domänenlogik

Wenn ein Problem natürlich durch Meta-Records, TypeFns, Refinements oder Protocols ausdrückbar ist, soll es nicht als Compiler-Annotation modelliert werden.

2.5 Unbekannte Annotationen

Eine unbekannte Compiler-Annotation ist ein Compilerfehler, sofern dieses Dokument oder eine spätere normative Ergänzung nicht explizit etwas anderes festlegt.

2.6 Keine semantische Schleichmagie

Eine Compiler-Annotation darf beobachtbares Programmverhalten nicht heimlich ändern, außer dies ist ihre explizit definierte Aufgabe und normativ beschrieben.

2.7 Klare Phasenzuordnung

Jede Compiler-Annotation muss genau angeben:

  • in welcher Compiler-Phase sie ausgewertet wird,
  • welche Eingaben sie lesen darf,
  • und welche Art von Wirkung sie erzeugt.

3. Pipeline-Phasen

Compiler-Annotationen wirken nicht alle zur selben Zeit. Ihre Semantik ist an die Compiler-Pipeline gebunden.

3.1 Phasenübersicht

Phase Beschreibung
Parse Quelltext wird in einen AST mit Annotation-Knoten geparst
Prune frühe AST-Reduktion vor Name Resolution
Resolve Name Resolution
Typecheck Typprüfung
Layout Typ-/Layoutinformationen vollständig verfügbar
Registry Sammlung von Test-/Hook-/Lang-Items
IR Build IR-Builder konsumiert bereits bereinigten AST
Lowering / Codegen Annotationen wirken hier nur noch indirekt

3.2 Phase: Parse

Bei Parse werden Annotationen als AST-Bestandteile erfasst. Ihre Präsenz allein hat noch keine Wirkung.

3.3 Phase: Prune

In der Prune-Phase werden Annotationen ausgewertet, die AST-Knoten vollständig entfernen können.

Normativ: @cfg wirkt in dieser Phase.

3.4 Phase: Typecheck / Layout

Annotationen, die Typ- oder Layoutwissen benötigen, dürfen erst ausgewertet werden, wenn die benötigten Informationen vollständig vorliegen.

Normativ:

  • @assert (early) wirkt vor oder unabhängig vom Typecheck,
  • @assert_type wirkt nach vollständiger Typ-/Layoutauflösung.

3.5 Phase: Registry

Registry-Annotationen sammeln deterministisch Symbole, ohne deren normale Sprachsemantik zu verändern.

Normativ:

  • @test
  • @lang_item

wirken in dieser Phase.

3.6 Beziehung zur IR-Pipeline

Der IR-Builder sieht nur den AST nach Pruning und nach den früh wirksamen Validation-Schritten.

Insbesondere:

  • durch @cfg entfernte Zweige existieren für Resolution, Typecheck und IR-Building nicht,
  • erfolgreiche @assert / @assert_type verändern den AST nicht,
  • Diagnose-Annotationen verändern den IR nicht.

4. Zulässige Annotation-Familien

Nur die in diesem Dokument beschriebenen Annotation-Familien sind Teil des Compiler-Annotationssystems.

4.1 @cfg(...)

4.1.1 Zweck

@cfg(...) aktiviert oder deaktiviert AST-Teile abhängig vom Build-Kontext.

4.1.2 Zulässige Positionen

Erlaubt:

  • auf Items,
  • auf Blöcken, die ausschließlich aus Items bestehen.

Nicht erlaubt:

  • auf beliebigen Ausdrücken,
  • mitten in normalen Expressions,
  • als allgemeiner Laufzeit-Branch-Ersatz.

4.1.3 Auswertungsphase

@cfg wird nach Parse und vor Name Resolution ausgewertet.

4.1.4 Semantik

Wenn das Prädikat true ist, bleibt der annotierte Knoten erhalten.

Wenn das Prädikat false ist, wird der annotierte Knoten vollständig aus dem AST entfernt.

4.1.5 Konsequenzen

Inaktive @cfg-Zweige:

  • müssen nicht typkorrekt sein,
  • nehmen nicht an Name Resolution teil,
  • erzeugen keinen IR.

4.1.6 Beispiele

@cfg(target.os == "linux")
pub def useEpoll() -> Result[(), IoError] = ...

@cfg(feature("test")) {
    @test
    def storage_roundtrip() -> Result[(), TestError] = ...
}

4.1.7 Fehlerfälle

Compilerfehler bei:

  • ungültigem Prädikat,
  • unzulässiger Position,
  • unbekannten Build-Kontext-Schlüsseln,
  • unbekannten Features.

4.2 @assert(...)

4.2.1 Zweck

@assert(...) erzwingt frühe, compile-time auswertbare Bedingungen, ohne Code umzuschreiben.

4.2.2 Auswertungsphase

@assert wird nach @cfg-Pruning ausgewertet.

4.2.3 Zulässige Eingaben

Erlaubt sind Prädikate gegen den Build-Kontext und die Predicate-DSL aus Abschnitt 5.

Keine Typ- oder Layout-Queries.

4.2.4 Semantik

Wenn das Prädikat true ist, geschieht nichts.

Wenn das Prädikat false ist, erzeugt der Compiler einen Fehler.

4.2.5 Beispiele

@assert(target.os == "linux", "This module requires Linux")
pub extern Epoll { ... }

@assert(feature("ffi"), "FFI support must be enabled")
pub def loadNativePlugin(path: str) -> Result[Plugin, PluginError] = ...

4.2.6 Fehlerfälle

Compilerfehler bei:

  • ungültigem Prädikat,
  • unzulässiger Position,
  • nicht auswertbarem Ausdruck,
  • fehlenden oder falschen Argumenten.

4.3 @assert_type(...)

4.3.1 Zweck

@assert_type(...) erzwingt Compile-Time-Bedingungen, die vollständige Typ- oder Layoutinformationen benötigen.

4.3.2 Auswertungsphase

@assert_type wird nach vollständiger Typauflösung und Layoutberechnung ausgewertet.

4.3.3 Zulässige Eingaben

Zusätzlich zur Predicate-DSL dürfen type-aware Queries verwendet werden, z. B.:

  • sizeof(T)
  • alignof(T)
  • layoutof(T)

4.3.4 Semantik

Wenn das Prädikat true ist, geschieht nichts.

Wenn das Prädikat false ist, erzeugt der Compiler einen Fehler.

4.3.5 Beispiele

@assert_type(sizeof(MyHeader) == 16, "MyHeader must remain ABI-stable")
pub type MyHeader: struct { ... }

@assert_type(alignof(Vec4) == 16, "Vec4 must stay 16-byte aligned")
pub type Vec4: struct { ... }

4.3.6 Fehlerfälle

Compilerfehler bei:

  • Nutzung vor verfügbarer Typ-/Layoutinformation,
  • unzulässigen Queries,
  • nicht auswertbaren Bedingungen.

4.4 @deprecated(...)

4.4.1 Zweck

@deprecated(...) markiert Symbole als veraltet und erzeugt Diagnosemeldungen bei Verwendung.

4.4.2 Wirkung

Die Annotation verändert keine Semantik.

Sie erzeugt eine Warnung bei Verwendung, nicht schon beim bloßen Import.

4.4.3 Zulässige Payload

Optional:

  • Nachricht,
  • since,
  • replacement.

4.4.4 Beispiele

@deprecated("Use newApi() instead")
pub def oldApi() -> Result[(), ApiError] = ...

@deprecated(message = "Use Effect.run()", since = "0.4", replacement = "Effect.run")
pub def runEffectNow[T](...) -> T = ...

4.4.5 Fehlerfälle

Compilerfehler bei ungültiger Payload-Form oder unzulässiger Position.


4.5 @test

4.5.1 Zweck

@test markiert Funktionen für Test-Discovery und Test-Registry.

4.5.2 Auswertungsphase

@test wirkt in der Registry-Phase.

4.5.3 Semantik

  • Der Compiler oder ein Tool sammelt annotierte Funktionen.
  • Die Funktion bleibt normale JDL-Funktion.
  • Test-Ausführung erfolgt in einem separaten Test-Modus.

4.5.4 Varianten

  • @test
  • @test.ignore("reason")
  • @test.only

4.5.5 Signaturregeln

Die zulässigen Testsignaturen sind vom Testsystem festgelegt.

Empfohlene MVP-Formen:

  • () -> ()
  • () -> Result[(), E]

4.5.6 Beispiele

@test
def parser_roundtrip() -> Result[(), TestError] = ...

@test.ignore("broken on current bridge draft")
def ffi_sqlite_smoke() -> Result[(), TestError] = ...

4.5.7 Fehlerfälle

Compilerfehler oder Registry-Fehler bei:

  • unzulässiger Signatur,
  • ungültiger Payload,
  • doppelter oder widersprüchlicher Test-Annotation.

4.6 @lang_item(...)

4.6.1 Zweck

@lang_item(...) bindet ein Symbol explizit an eine vom Compiler oder der Runtime erwartete Sonderrolle.

4.6.2 Anwendungsfälle

Beispiele:

  • main
  • globaler Panic-/Error-Handler
  • Default-Storage-/Allocator-Provider
  • Modul-Init-/Deinit-Hooks

4.6.3 Auswertungsphase

@lang_item wirkt in der Registry- bzw. Link-Phase.

4.6.4 Semantik

Ein @lang_item(name) markiert eine Definition als Träger genau dieser Rolle.

Normativ:

  • pro Rolle ist höchstens eine gültige Definition pro Link-Unit erlaubt,
  • die erwartete Signatur ist fest vorgegeben,
  • fehlende Pflicht-Items sind Compiler- oder Link-Fehler,
  • optionale Lang-Items dürfen Default-Verhalten haben.

4.6.5 Beispiele

@lang_item("main")
def appMain() -> i32 = ...

@lang_item("panic_handler")
def handlePanic(p: PanicInfo) -> Never = ...

4.6.6 Fehlerfälle

Compiler- oder Link-Fehler bei:

  • doppelter Definition derselben Rolle,
  • falscher Signatur,
  • unzulässiger Platzierung,
  • fehlendem Pflicht-Lang-Item.

4.7 Optionale Hint-Annotationen

Die folgenden Annotationen sind optionale, nicht-semantische Hints:

  • @inline
  • @noinline
  • @lint.allow(...)
  • @lint.deny(...)

4.7.1 Regel

Hints dürfen beobachtbares Verhalten nicht verändern.

4.7.2 Status

Nicht Teil des minimalen garantierten MVP, sofern nicht separat festgelegt.


5. Predicate-DSL

Die Predicate-DSL wird von @cfg(...) und frühem @assert(...) verwendet.

5.1 Ziele

Die DSL muss:

  • deterministisch,
  • klein,
  • parserfreundlich,
  • und unabhängig von Runtime-Semantik sein.

5.2 Zulässige Elemente

Erlaubt:

  • Bool-Literale,
  • String-Literale,
  • Integer-Literale,
  • ==, !=,
  • optional <, <=, >, >= für numerische Werte,
  • and, or, not bzw. äquivalente Operatorformen,
  • Klammerung,
  • Build-Kontext-Zugriffe,
  • feature("name").

5.3 Build-Kontext-Zugriffe

Beispiele:

  • target.os
  • target.arch
  • build.mode
  • build.debug

Die Menge zulässiger Schlüssel ist compilerseitig festgelegt.

5.4 Feature-Abfrage

feature("ffi")
feature("test")

liefert einen booleschen Wert aus dem Build-Kontext.

5.5 Nicht erlaubt

Nicht erlaubt:

  • Funktionsaufrufe außerhalb der fest definierten DSL,
  • Zugriff auf Runtime-Werte,
  • Typqueries in @cfg oder frühem @assert,
  • Seiteneffekte.

5.6 Fehlerregeln

Unbekannte Schlüssel oder Features sind Compilerfehler, keine stillen false-Werte.


6. Beziehung zu anderen Mechanismen

6.1 @ vs. Typ-Meta

Compiler-Annotationen sind nicht das normale Meta-System von JDL.

Typ-Meta beschreibt:

  • Policies,
  • Typkonstruktion,
  • Verhalten,
  • Constraints,
  • kompositorische Sprachsemantik.

Compiler-Annotationen beschreiben dagegen:

  • frühe Compilerentscheidungen,
  • Diagnose,
  • Registry,
  • Runtime-Hooks.

6.2 @cfg vs. type-level if

@cfg entfernt AST-Knoten vor Resolution/Typecheck.

Ein type-level if ist normale Typsemantik und entfernt keinen AST.

6.3 @ vs. Intrinsics

Intrinsics sind compiler- oder VM-provided Funktionen bzw. TypeFns an einer stabilen Sprach-/Runtime-Grenze.

Compiler-Annotationen bleiben AST- bzw. Compiler-Pipeline-Konzepte.

Beispiel:

  • @cfg(...) = AST-Pruning,
  • labels_of[T] = type-level Intrinsic,
  • jdl.vm_pin(...) = Runtime-Intrinsic.

6.4 @ vs. IR-Desugaring

Compiler-Annotationen sind keine IR-Instruktionen.

Sie wirken vor oder neben dem IR-Building und transportieren ihre Wirkung höchstens indirekt in den IR.

6.5 @lang_item vs. normale Sprachbindung

@lang_item ist keine Alternative zu provide, service, protocol oder normaler Modul-Semantik.

Es markiert nur Spezialrollen, die der Compiler oder die Runtime explizit verdrahtet erwarten.


7. Fehlerregeln und Diagnostik

7.1 Allgemeine Regel

Fehlerhafte Compiler-Annotationen müssen präzise Diagnosen erzeugen.

Die Diagnose soll mindestens enthalten:

  • welche Annotation fehlerhaft ist,
  • warum sie fehlerhaft ist,
  • an welcher Stelle,
  • und wenn möglich, was stattdessen zulässig wäre.

7.2 Typische Fehlerklassen

  • unbekannte Annotation,
  • ungültige Position,
  • falsche Payload,
  • ungültiges oder nicht auswertbares Prädikat,
  • mehrfache @lang_item-Definition,
  • unzulässige Testsignatur,
  • Nutzung einer layoutabhängigen Assertion vor Layout-Phase.

7.3 Diagnoseprinzipien

  • keine stillen Fallbacks,
  • keine implizite Ignorierung unbekannter Annotationen,
  • Build-Kontext-Fehler sind hart,
  • inaktive @cfg-Zweige werden nicht typgeprüft und erzeugen daher auch keine Typfehler aus entferntem Code.

8. MVP-Whitelist

Die folgende Menge ist Teil des minimalen, stabil anzustrebenden MVP:

  • @cfg(...)
  • @assert(...)
  • optional @assert_type(...)
  • @deprecated(...)
  • @test
  • @test.ignore(...)
  • @test.only
  • @lang_item(...)

Nicht Teil des harten MVP, sofern nicht separat festgelegt:

  • @mock
  • @inline
  • @noinline
  • @lint.allow(...)
  • @lint.deny(...)

9. Offene Punkte

9.1 @lang_item-Scope

Offen ist, ob @lang_item-Eindeutigkeit pro Modul, pro Package oder pro vollständiger Link-Unit gilt.

Empfohlene Richtung: Eindeutigkeit pro Link-Unit.

9.2 Zulässige Build-Kontext-Schlüssel

Die exakte normative Liste von:

  • target.*
  • build.*
  • feature(...)

muss separat fixiert werden.

9.3 Testsignaturen

Die endgültig erlaubten Signaturen für @test sind noch festzulegen.

9.4 @assert / @assert_type Trennung

Offen ist, ob beide dauerhaft getrennte Annotationen bleiben oder eine spätere, explizit gestufte Einzelform eingeführt wird.

9.5 Lint-Hints

Offen ist, ob Lint-Annotationen Teil des Kernsystems oder nur Tooling-Sache werden.


10. Rationale

10.1 Warum keine Makros

Makros etablieren oft eine zweite, schlechter spezifizierte Sprachebene. Sie erschweren:

  • Debugging,
  • Tooling,
  • Diagnosequalität,
  • und semantische Lesbarkeit.

JDL bevorzugt stattdessen:

  • normales Typ-Meta,
  • TypeFns,
  • Protocols,
  • kleine Compiler-Annotationen,
  • und klar begrenzte Intrinsics.

10.2 Warum @cfg kein Präprozessor sein soll

@cfg ist absichtlich auf frühes, strukturelles AST-Pruning begrenzt. Es soll keine beliebige Quelltext-Magie werden.

10.3 Warum @ klein bleiben muss

Je mehr Logik nach @ wandert, desto mehr verliert JDL seine eigene sprachliche Ausdruckskraft zugunsten einer compilerseitigen Sonderwelt.

Die zentrale Regel lautet daher:

So viel wie möglich in normaler JDL-Semantik.
@ nur dort, wo die Sprachgrenze bewusst und deterministisch markiert werden muss. ```