Zum Inhalt

JDL Spec — Namespaces & Modulsystem

Status: Draft (konsolidiert, Syntax an den aktuellen JDL-Stand angepasst)
Scope: Module als Namespaces, ::-Auflösung, Import/Export, mod.jdl-Re-Exports, Konfliktregeln, relative Pfade (parent::, package::), Fully-Qualified Names, reservierte Namespaces und Prelude.
Out of scope: Paketmanager, Versionierung, Diamond-Resolution, Build-System.


1. Begriffe

  • Symbol: Name einer Definition (z.B. User, findUser).
  • Modul: Quelltext-Einheit, die über einen Dateipfad identifiziert wird.
  • Namespace: Ein Namensraum, in dem Symbole eindeutig adressierbar sind. In JDL sind Module Namespaces.
  • Qualified Name: Name mit Namespace-Pfad, z.B. users::service::findUser.
  • Fully Qualified Name (FQN): Qualified Name ab Root, ohne implizite Präfixe.
  • Package: Logische Distributions-Einheit (für Coherence/Orphan-Regel relevant). Details sind Paketmanager-Sache, aber „gleiches Package/Modul“ ist eine normative Grenze.

2. Separator-Regel: :: vs .

  • :: ist der Namespace-Separator (Module, Type-Namespaces, Intrinsics-Namespaces).
  • . ist Member-/Property-Zugriff (z.B. user.email, vec.len()), nicht für Namespaces.

Normativ: Namespaces werden nie mit . getrennt.


3. Modul- und Namespace-Identität

3.1 Dateipfad → Modulname

  • Der Dateipfad ist der Modulname.
  • Eine Datei users/service.jdl definiert das Modul users::service.
  • Es gibt keine Moduldeklaration innerhalb der Datei; der Pfad ist die Quelle der Wahrheit.

3.2 Zirkuläre Abhängigkeiten

  • Zirkuläre Modulabhängigkeiten sind ein harter Compilerfehler.

4. Sichtbarkeit

4.1 Sichtbarkeitsstufen

  • Ohne Sichtbarkeits-Keyword ist eine Definition privat (nur innerhalb des Moduls sichtbar).
  • Mit package ist sie in allen Modulen desselben Packages/Plugins sichtbar, aber nicht extern exportierbar.
  • Mit pub ist sie exportiert und für andere Module importierbar (direkt oder via Re-Export).

Beispiel:

// users/service.jdl

def validateEmail(email: str) -> bool { ... }  // privat, nur innerhalb des Moduls verwendbar
package def normalizeEmail(email: str) -> str

pub def findUser(email: str) -> Result[User, UserError]
pub type User: struct { ... }
pub type UserError: enum = | NotFound | AlreadyExists

5. Imports

5.1 Import-Formen (kanonisch)

Modul importieren (als Modulname im Scope):

import users::service
import users::service as user_service

Symbole aus einem Modul importieren:

import users::service : User, findUser
import users::service : User as AppUser 
import std::crypto : Hasher as enc 

Normativ: - Es gibt keinen impliziten „Wildcard“-Import. Explizit ist Pflicht. - Wenn eine Importform import a::b::c : Y, Z nutzt, ist das die bevorzugte Schreibweise für „gezielt und lesbar“.

5.2 Relative Auflösung mit parent:: und package::

  • parent:: referenziert den Parent-Namespace des aktuellen Moduls.
  • package:: referenziert das Root-Modul des aktuellen Packages/Plugins.

Beispiele:

// users/mod.jdl
pub import package::model : User, UserId
pub import package::service : findUser, registerUser
// users/http/router.jdl
import parent::middleware : Middleware
import package::model     : User, UserId

Normativ: - parent::x und package::x sind relative Namespace-Referenzen. - parent:: darf nur verwendet werden, wenn ein Parent-Namespace existiert. Die Verwendung von parent:: in einem Top-Level-Modul ist ein Compilerfehler. - package:: ist immer relativ zum Root-Modul des aktuellen Packages/Plugins.

5.3 Importbarriere für internal

  • Ein Modulpfad der ein Segment internal enthält, ist außerhalb des aktuellen Packages/Plugins nicht importierbar.
  • pub in einem internal-Modul bedeutet daher: sichtbar innerhalb des Packages/Plugins, nicht automatisch extern importierbar.

Beispiele:

// users::internal::validator
pub def normalizeEmail(email: str) -> str = ...

Normativ: - import users::internal::validator : normalizeEmail ist innerhalb desselben Packages/Plugins zulässig. - Derselbe Import ist außerhalb des Packages/Plugins ein Compilerfehler. - mod.jdl bleibt API-Shaping für das Root-Modul; internal ist die harte Importbarriere.

5.4 Lokale Imports

  • Imports dürfen innerhalb von Funktionen vorkommen.
def parseConfig(path: str) -> Result[Config, ParseError] {
    import std::formats::toml : parse 
    parse(path)
}

5.5 Einschränkung: keine lokalen Imports von service-Typen

  • Lokale Imports von Service-Typen sind verboten.
  • Services werden als explizite Parameter (imperativer Stil) oder über Effect[R, E, D] im Rückgabetyp (Effect-Stil) deklariert — nie per lokalem Import.

Beispiel (muss fehlschlagen):

def findUser(email: str) -> Result[User, UserError] {
    import myapp::services : UserRepo   // COMPILERFEHLER: service-Typ lokal importiert
    ...
}

5.6 Tiny Prelude

  • JDL kennt einen kleinen impliziten Prelude.
  • Dieser Prelude ist kein impliziter Wildcard-Import von jdl::core.
  • Der Compiler bringt nur eine bewusst kleine Menge kanonischer Kern-Typen automatisch in Scope.

Normativ: - Result und Option sind kanonisch in jdl::core definiert. - Der Compiler bringt Result und Option als bootstrap-preloaded Teilmenge automatisch in Scope, ohne dass ein expliziter Import nötig ist. - Primitive Builtins (i8..i64, u8..u64, f32, f64, bool, str, ()) sind keine normalen Prelude-Imports, sondern Sprach-Builtins.


6. Fully Qualified Names (FQN) und „Full Qualified Imports“

6.1 Fully Qualified Usage (ohne Import)

Ein Symbol darf per FQN verwendet werden, ohne es vorher ins lokale Scope zu importieren:

let u = users::service::findUser("a@b.c")
type UserUpdate = std::typefn::Partial[users::model::User, [.id, .name]]

Normativ: - Der Compiler muss FQN-Referenzen als Abhängigkeiten im Modulgraphen erfassen (wie bei Imports).

6.2 „Full Qualified Import“ (Symbol direkt importieren)

Als sugar darf ein Symbol auch direkt über seinen vollständigen Pfad importiert werden:

import users::service::UserService
import users::model::User as AppUser

Semantik: - import A::B::x ist äquivalent zu import A::B : x (ggf. mit Alias).


7. mod.jdl — optionale API-Kontrolle (Re-Exports)

7.1 Semantik

  • mod.jdl ist eine normale JDL-Datei mit folgender Konvention:
  • dir/mod.jdl repräsentiert das Modul dir.
  • Sie dient dazu, die public API des Root-Moduls über Re-Exports zu formen.

Normativ: - Ohne mod.jdl: Das Root-Modul des Ordners exportiert alle pub-Definitionen, als wären sie Teil seiner öffentlichen Oberfläche. - Mit mod.jdl: Die Re-Exports in mod.jdl definieren die öffentliche Oberfläche des Root-Moduls.

Wichtig: - mod.jdl ist API-Shaping für das Root-Modul. - Es ist keine Zugriffskontrolle gegen direkte Imports von Submodulen. Ein Submodul kann weiterhin via import users::validator importierbar sein, wenn es dort pub-Symbole gibt (sofern nicht anders spezifiziert).

Beispiel:

// users/mod.jdl
pub import package::model : User, UserId
pub import package::service : findUser, registerUser
// validator.jdl bleibt nicht Teil der Root-API, ist aber ggf. direkt importierbar

7.2 Transitive Sichtbarkeit (Re-Exports)

  • Re-Exports sind transitiv sichtbar.
  • Ein Konsument muss das Originalmodul nicht separat importieren.
// users/mod.jdl
pub import std::types : UserId 

// anderswo
import users : User, UserId   // UserId kommt transitiv aus std::types

8. Namensauflösung und Konflikte

8.1 Konfliktregel (unqualifiziert)

  • Wenn zwei Imports dasselbe Symbol ins unqualifizierte Scope bringen, ist das ein Compilerfehler.

import std::collections : Map 
import mylib::types : Map      // COMPILERFEHLER: Map kollidiert

import mylib::types : Map as MyMap  // ok

9. Reserved Namespaces

9.1 Stdlib- und System-Namespaces

  • jdl:: ist der user-facing Root-Namespace der JDL-Stdlib.
  • jade:: ist der reservierte interne System-/Runtime-Namespace für privilegierte JDL-Systemmodule.

9.2 Compiler-provided Namespace: @compiler

  • @compiler ist ein compiler-provided Namespace.
  • Er liefert compile-time Queries/Primitives, die nicht sauber als normale Library ausgedrückt werden können, z.B.:
  • Target/Build/Feature-Fakten: @compiler::target::*, @compiler::build::*, @compiler::feature("x")
  • Layout/Type Queries (später): @compiler::sizeof(T), @compiler::alignof(T), @compiler::layoutof(T)

Normativ: - Namespaces mit @-Prefix sind für Compiler/Toolchain reserviert. - @intrinsics ist kein kanonischer Namespace mehr; bestehende Verwendungen sind historische Altlasten und auf @compiler zu normalisieren.


10. Zusammenhang mit provide (Coherence / Orphan-Regel)

Damit provide P for T nicht zur Import-Lotterie wird:

  • Eine Protocol-Implementierung ist nur erlaubt, wenn P oder T im selben Package oder Modul definiert ist.
  • Für fremde Typen nutzt man Newtypes.

11. Offene Punkte (bewusst minimal gehalten)

  1. Soll es zusätzlich zu parent:: und package:: weitere relative Segmente geben? Aktueller Stand: nein.
  2. Falls ein projektweiter Opt-out vom Tiny Prelude gewünscht ist, muss die Manifest-/Compiler-Flag-Seite spezifiziert werden.
  3. internal ist als harte package-/plugin-interne Importbarriere normiert; offene Folgefrage bleibt nur, ob zusätzlich weitere Zugriffsstufen nötig sind.