Jade / JDL — Designentscheidungen¶
Status: Informative Design-Notizen / historischer Kontext
Stand: aktualisierte Konsolidierungsfassung
Geltungsbereich: Architektur- und Sprachentscheidungen, die noch nicht vollständig in normative Specs überführt wurden.Dieses Dokument ist nicht normativ. Es beschreibt Designentscheidungen, Arbeitskonsens und technische Richtung. Bei Konflikten gelten die konsolidierten Spezifikationen, insbesondere Typsystem, Refinements, TypeFns, Tags/Phantoms, Runtime/JME, Plugin-System, IR, CompilerDb und VM-Spezifikationen.
0. Konsolidierungsstand¶
Die alte Fassung dieses Dokuments enthielt mehrere historisch gewachsene Skizzen. Diese Fassung normalisiert die Beispiele auf den aktuellen JDL-Kurs:
defstattfn.- Funktionstypen immer mit runden Klammern:
(T) -> U,() -> U. X -> Ybleibt Cast-/Flow-Schreibweise und ist kein Funktionstyp.- Refinements werden user-facing über
:>und Refinement-TypeFns geschrieben. - Direkte Meta-Record-Literale wie
<{ ... }>gelten hier nur als erklärende interne Form, nicht als empfohlene Oberflächensyntax. Rcersetzt die frühere BezeichnungReffür refcounted Speicher.Array[T]ersetzt alteVec[T]-Namen.- Keine rohen Pointer in JDL; FFI nutzt explizite, gekapselte Grenztypen.
- Blueprints, Newtypes,
&, Type-Level-Normalisierung und Row-Polymorphismus werden als zentrale Typalgebra-Werkzeuge berücksichtigt. tag,phantom,reified,dynund zero-field Newtypes bleiben ontologisch getrennt.
Merksatz:
1. Operator .., Intervalle und reaktive Driver¶
Entscheidung¶
.. ist ein normaler protokollgebundener Operator. Er ist kein Sonderfall im Compiler.
Der Operator beschreibt eine Verbindung zwischen zwei Werten. Welche Bedeutung diese Verbindung hat, entscheidet das Protocol des Typs:
Steppable[T]erzeugt diskrete Bereiche:Range[T].Interpolatable[T]erzeugt kontinuierliche Übergänge:Interval[T].
Interval[T] ist der bevorzugte Name für kontinuierliche Übergänge. Der frühere Name Span[T] bleibt historischer Kontext, ist aber semantisch schlechter, weil „Span“ in Systemprogrammierung bereits zu viele Bedeutungen mitbringt. Ja, natürlich war ein weiterer überladener Begriff genau das, was die Welt gebraucht hätte.
JDL-Skizze¶
protocol Interpolatable[T] {
def interpolate(self, other: T, t: f64) -> T
}
protocol Spannable[T] {
def span(self, other: T) -> Interval[T]
} :> Operator("..")
provide[T] Spannable[T] for T
where T: Interpolatable[T] + Copyable
{
def span(self, other: T) -> Interval[T] =
Interval { from: self, to: other }
}
Verwendung:
Driver-Prinzip¶
Die Pipeline bleibt pure. Effekte kommen von außen durch Driver.
val gradient = Color.green..Color.red
gradient.driveWith(Clock.tick(16ms))
gradient.driveWith(scrollPosition())
gradient.driveWith(audioLevel())
Das ergibt reaktive Systeme als emergente Eigenschaft, nicht als eigenes Sprachfeature. Genau so sollte Sugar riechen: nützlich, aber nicht nach Compiler-Sondermüll.
2. Parser-Kombinatoren¶
Entscheidung¶
Jade verwendet für den Bootstrap-Compiler einen Parser-Kombinator-Ansatz, keinen Parser-Generator.
Kernpunkte:
- Kombinatoren:
once,many,optional,sequence,choice. - D-Implementierung bevorzugt
@nogc,nothrow,pure, wo sinnvoll. structstattclass, um GC-Overhead und Vererbungsbäume zu vermeiden.ref-Parameter statt roher Pointer in D-internen Hot-Paths, soweit möglich.- D-Slices dienen als Zero-Copy-Fenster in bestehende Source-Buffer.
- AST-Knoten sollen als Sum Types/Tagged Unions modelliert werden, nicht als Klassenhierarchie.
- Fehler laufen über strukturierte
Diagnostic-Werte.
Ergebnis-Typ im Bootstrap-Compiler¶
Im Bootstrap-Compiler ist JadeResult[T] der einheitliche Ergebnistyp:
Ok(T) Ergebnis vorhanden
Err(Diagnostic) Fehler mit Diagnose
Empty korrekt ausgeführt, aber kein Wert vorhanden
Empty ersetzt im Bootstrap-Compiler viele Option[T]-Anwendungen. Das ist eine Compiler-Implementierungsentscheidung, keine allgemeine JDL-Stdlib-Regel.
3. Diagnostics und Fehlermeldungen¶
Entscheidung¶
Diagnostik ist ein eigener First-Class-Entwurfsbereich, kein nachträgliches println mit Größenwahn.
struct Diagnostic {
Phase phase;
Severity severity;
SourceSpan span;
string message;
Hint[] hints;
Note[] notes;
}
struct Hint {
SourceSpan span;
string message;
}
struct Note {
string message;
}
Kernregeln:
- Jede Compilerphase kann eigene Diagnosen erzeugen.
- Hints dürfen auf andere Source-Spans zeigen als der primäre Fehler.
- Notes erklären Zusammenhänge ohne eigenen Source-Span.
- Diagnosen müssen die TypeEngine-/Closure-/Context-Informationen nutzen können.
- Label- und Type-Algebra-Fehler sollen Proof-Traces bzw. Begründungsketten liefern.
Beispielhafte Diagnoseidee:
Fehler: `UserIndex` kann `Send` nicht erfüllen.
Grund:
Feld `cache` trägt `Share(Local)`.
Benötigt durch:
spawn(...) verlangt `Share(Send)` für alle Captures.
Hinweis:
Verwende einen `SyncCache`, einen Actor, oder bewege den Wert nicht über die Task-Grenze.
Das ist die Sorte Fehler, bei der der Compiler nicht nur „nein“ sagt, sondern wenigstens erklärt, warum er wieder einmal recht hat.
4. Memory Policies und JME-Verhalten¶
Entscheidung¶
Rc ersetzt die alte Bezeichnung Ref.
Ref klang zu sehr nach Referenz. Rc beschreibt die tatsächliche Speicherstrategie: refcounted ownership über die Jade Memory Engine.
Primitive JME-Verhaltensweisen¶
| Policy | Semantik | Typischer Einsatz |
|---|---|---|
Value |
inline, kein Handle, kein JME-Objekt | kleine primitive/Plain-Data-Typen |
Rc |
refcounted Heap-Wert | str, Array[T], dynamische Strukturen |
Weak |
nicht-besitzender Verweis, upgrade() -> Option[T] |
Parent-Links, Zyklusvermeidung |
Arena[A] |
Bump-Allokation, Bulk-Reset | Request-/Frame-/Analyse-Daten |
Pool[P] |
Freelist/Slab-Wiederverwendung | Verbindungen, Tasks, teure Objekte |
Alles andere ist Komposition aus Typstruktur, Refinements, Labels und JME-Invarianten.
type UserStats: struct {
totalUsers: i64
activeUsers: i64
avgAge: f64
} :> Memory(Value)
type User: struct {
name: str
email: str
age: i32
} :> Memory(Rc)
Die explizite Policy ist optional, wenn die TypeEngine sie ableiten kann. Explizit wird sie vor allem dann, wenn der Entwickler eine bestimmte Speicherstrategie erzwingen oder dokumentieren will.
5. Arenen und with¶
Entscheidung¶
Arenen brauchen kein eigenes ArenaScope-Protocol auf JDL-Oberfläche. with ist ein Sprachkonstrukt für deterministisches Scoping.
Arena-Typen werden über Refinements an die JME gebunden:
Konzeptionell:
open() -> Speicherblock reservieren, cursor = base, Arena auf Scope-Stack pushen
alloc() -> cursor += size
reset() -> cursor = base
close() -> reset + free + vom Scope-Stack entfernen
Verwendung:
def handleRequest(storage: Storage, req: HttpRequest) -> Result[HttpResponse, AppError] {
with storage.arena[RequestArena] {
val parsed =? parseRequest(req)
val body = renderResponse(parsed)
Ok(HttpResponse { body })
}
}
Grundregeln:
- Werte aus einer Arena dürfen die Arena nicht als Referenz verlassen.
- Rückgabe aus einem Arena-Scope kopiert in die Zielregion, wenn nötig und zulässig.
- Nicht-kopierbare Werte können Arena-Grenzen nicht via Return verlassen.
- Ressourcen mit Custom-Drop gehören normalerweise nicht in kurzlebige Datenarenen, sondern in eigene
with-Scopes.
6. Slices und Views¶
Entscheidung¶
Slices sind Views, solange der Ursprung gültig bleibt. Über Scope-Grenzen hinweg wird kopiert, sofern das Typsystem die Kopie erlaubt.
Über Scope-Grenze:
Wenn der Slice nicht sicher als View zurückgegeben werden kann, materialisiert der Compiler eine Kopie in der Zielregion. Wer das vermeiden will, gibt einen Generator[T] zurück.
Das ist ergonomisch, nicht maximal low-level. Wer jede Kopie selbst verwalten will, darf gerne mit FFI und Messinghelm in den Keller steigen.
7. Strings¶
Entscheidung¶
Strings sind UTF-8 und immutable by default.
Kernpunkte:
strist JME-verwaltet, typischerweiseMemory(Rc).- Mutation erzeugt eine neue Repräsentation oder nutzt explizite Builder.
- Keine rohen Pointer in JDL; FFI-Grenzen nutzen explizite FFI-Typen und Cast-/Wire-Regeln.
Interning¶
Compile-Zeit:
String-Literale -> Interning-Table im Artefakt
Runtime:
kurze Strings können optional interned werden
lange Strings werden nicht automatisch interned
StringBuilder¶
protocol Buildable {
type Item
type Output
def append(self, value: Self.Item) -> Self
def build(self) -> Self.Output
}
val s = str.builder()
.append("Hello, ")
.append(name)
.append("!")
.build()
Associated Types vermeiden hier generischen Lärm. Ja, manchmal ist Typentheorie tatsächlich ergonomisch. Widerlich.
8. Collections: Array-Familie¶
Dynamisches Array¶
Array[T] ist die dynamisch wachsende Heap-Collection.
Eigenschaften:
- Wachstum kann Reallokation benötigen.
- JME kann Speicher intern bewegen; Handles bleiben stabil.
withCapacityist ein Optimierungskonstruktor, keine semantische Pflicht.
Statische Buffer¶
typefn StackBuffer[T, N]
where N: Literal[usize]
=
struct {
data: [T; N]
filled: usize
} :> Memory(Value)
Verwendung:
Notation¶
[T] Slice / View, kein Ownership
[T; N] statisches Array mit Compile-Time-Größe
Array[T] dynamische, heap-verwaltete Collection
9. Map[K, V] als typalgebraische Collection-Familie¶
Alte Idee, modern gefasst¶
Die alte Entscheidung war: Map[K, V] ist kein einzelner primitiver Typ, sondern ein Default auf eine konkrete Implementierung wie HashMap[K, V].
Die aktualisierte Fassung geht weiter:
Map ist eine materialisierbare Collection-Familie.
Ihre konkrete Form kann durch TypeFns, Blueprints, Newtypes, Refinements und Type-Algebra beschrieben werden.
Damit ist Map[K, V] nicht nur „HashMap mit freundlichem Namen“, sondern Teil eines konsistenten Modells:
- Protocol beschreibt Verhalten.
- TypeFn wählt Default-Repräsentation.
- Blueprint beschreibt Strategie/Policy.
&spezialisiert Blueprint- oder Record-Shapes.- Newtype materialisiert eine domain-spezifische TypeId.
- Refinements setzen Memory-/Wire-/Share-/Drop-Policies.
- Row-Polymorphismus erlaubt APIs, die nur benötigte Fähigkeiten verlangen.
Verhalten: MapLike¶
Collection[K, V] war zu allgemein. Für key/value-Strukturen ist MapLike präziser.
protocol MapLike {
type Key
type Value
def get(self, key: Self.Key) -> Option[Self.Value]
def contains(self, key: Self.Key) -> bool
def len(self) -> usize
}
Mutable und persistente Maps werden getrennt:
protocol MutableMap: MapLike {
def set(self mut, key: Self.Key, value: Self.Value) -> ()
def delete(self mut, key: Self.Key) -> bool
}
protocol PersistentMap: MapLike {
def set(self, key: Self.Key, value: Self.Value) -> Self
def delete(self, key: Self.Key) -> Self
}
Das vermeidet die alte API-Unschärfe, bei der set gleichzeitig wie Mutation und persistent update roch. Diese Mischung ist nicht Flexibilität, sondern Semantik mit Tarnkappe.
Konkrete Implementierungen¶
Zusätzliche Constraints werden durch die jeweilige Implementierung verlangt:
HashMap[K, V] verlangt K: Hashable + Equatable
TreeMap[K, V] verlangt K: Comparable
SmallMap[K, V, N] verlangt K: Equatable und N: Literal[usize]
Konzeptionell:
Wichtig: SmallMap darf Memory(Value) nur tragen, wenn K, V und N diese Repräsentation zulassen. Das muss die TypeEngine ableiten oder prüfen. Wunschdenken ist kein Layout-Algorithmus.
Default: Map[K, V]¶
Regel:
Öffentliche APIs, persistente Speicherung und Wire-Grenzen sollen entweder eine konkrete Implementierung oder ein explizites Blueprint/Newtype-Modell verwenden.
Blueprint: MapSpec¶
Eine Map-Strategie ist deklarativ beschreibbar. Dafür eignet sich ein Blueprint.
type MapStrategy: enum =
| Hash
| Tree
| Inline
type OrderingPolicy: enum =
| Unordered
| InsertionOrdered
| Sorted
type Mutability: enum =
| Mutable
| Persistent
type MapSpec[K, V]: blueprint {
strategy: MapStrategy = .Hash
ordering: OrderingPolicy = .Unordered
mutability: Mutability = .Mutable
storage: StoragePolicy = .Rc
}
Spezialisierung über &:
type TreeMapSpec[K, V] =
TreeMapSpec(
MapSpec[K, V] & {
strategy: .Tree
ordering: .Sorted
}
)
type SmallMapSpec[K, V, N] =
SmallMapSpec(
MapSpec[K, V] & {
strategy: .Inline
storage: .Value
}
)
& erweitert hier nicht beliebig. Es spezialisiert bestehende Felder. Rechts gewinnt bei kompatiblen Feldkollisionen.
Domain-Newtypes über Maps¶
Bedeutung:
Das ist wichtig für:
- Coherence,
- domänenspezifische Methoden,
- API-Grenzen,
- Wire-/Storage-Policies,
- semantische Trennung gleich strukturierter Maps.
Beispiel:
Ein Alias wäre dagegen nur ein Name:
Der Alias ist dieselbe Map. Der Newtype ist eine neue Domäne. Kleine Syntax, große Konsequenz. So funktioniert gutes Sprachdesign gelegentlich, sehr zur Irritation aller Beteiligten.
10. Bäume und Hierarchien¶
Entscheidung¶
Parent/Child-Strukturen nutzen Rc und Weak, um Ownership und Back-References sauber zu trennen.
Regeln:
- Parent besitzt Children über
Rc. - Child kennt Parent über
Weak. Weak.upgrade()liefertOption[T].- Refcount-Zyklen werden dadurch vermieden.
Alternative Designs mit Actor-State, Arena-Bäumen oder Index-basierten Graphen bleiben möglich. Die Standardempfehlung ist kein Dogma, nur weniger dumm als zyklische starke Referenzen.
11. Tags, zero-field Newtypes und Witness Tokens¶
Entscheidung¶
JDL trennt drei Konzepte:
tag X
Compile-Time-Bedeutung, kein Wert, kein Layout.
type X = X({})
runtime-materialisierbarer Wert ohne Nutzdaten.
type T[phantom X]
Werttyp trägt eine compile-time Bedeutung ohne Runtime-Feld.
Tags¶
Tags sind Bedeutungen. Sie können nicht konstruiert oder als Wert übergeben werden.
Phantom-Zustand¶
Zero-field Newtype¶
AdminToken ist ein echter Werttyp mit eigener TypeId, aber ohne Nutzdaten. Das ist nützlich für:
- Capability Tokens,
- Witness Values,
- Proof Tokens,
- API-Gates,
- State Tokens mit realer Übergabe.
Beispiel:
Regel:
Nur Bedeutung? tag.
Bedeutung an Werttyp binden? phantom.
Konkreter Beweis-/Berechtigungswert nötig? zero-field Newtype.
12. Composability als Kernprinzip¶
Entscheidung¶
Jade implementiert wenige primitive Verhaltensweisen in Ring 0/1. JDL komponiert daraus höhere Semantik.
Beispiel:
type SensitiveData: struct {
payload: [u8]
} :> Memory(Arena[SecureArena])
:> Share(Local)
:> Derive([!Copyable, !Inspectable])
:> Create(Factory[SecureVault])
:> Drop(Custom)
Interpretation:
Memory(Arena[SecureArena])
physisch kontrollierte Speicherregion
Share(Local)
keine Task-Grenze
!Copyable
keine implizite Kopie
!Inspectable
keine automatische Debug-/Inspect-Ausgabe
Drop(Custom)
explizites Zeroing oder eigene Freigabelogik
Der Benutzer deklariert Absicht. Die TypeEngine leitet Garantien ab. Labels, TruthProfile und Verifier entscheiden, was tatsächlich erlaubt ist.
13. Effect-orientierte Stdlib-Bausteine¶
Status: Informativ. Stdlib-Planung, nicht normativ.
Schedule[E]¶
Ein Schedule[E] beschreibt Retry-/Backoff-Entscheidungen.
def exponentialBackoff[E](base: Duration) -> Schedule[E] =
(attempt, _) => Some(base * 2.pow(attempt))
def maxRetries[E](n: u32) -> Schedule[E] =
(attempt, _) => if attempt < n then Some(0ms) else None
def both[E](a: Schedule[E], b: Schedule[E]) -> Schedule[E] =
(attempt, error) => match (a(attempt, error), b(attempt, error)) {
| (Some(d1), Some(d2)) => Some(d1.max(d2))
| _ => None
}
Keine VM-Magie. Nur Funktionstypen und Composition.
RequestResolver¶
Ein RequestResolver bündelt gleichartige Requests automatisch, um N+1-Probleme zu vermeiden.
protocol Batchable {
type Request
type Response
def batch(requests: [Self.Request]) -> [Self.Response]
}
Die Scheduler-/Effect-Engine kann Tasks parken, gleichartige Requests sammeln und danach gemeinsam fortsetzen. Das ist Runtime- und Stdlib-Logik, kein neues Sprachkonstrukt.
Cause[E]¶
Cause[E] erhält parallele und sequenzielle Fehlerursachen.
type Cause[E]: enum =
| Fail(E)
| Interrupt
| Sequential(Cause[E], Cause[E])
| Parallel(Cause[E], Cause[E])
Nützlich für parallele Graphen und Effects, bei denen mehrere Fehler gleichzeitig entstehen können.
FiberRef[T]¶
FiberRef[T] bleibt deferred.
Begründung:
- Ambient State ist gefährlich bequem.
- Viele Use Cases lassen sich über Services, Effects, Actors oder ContextDescriptoren sauberer ausdrücken.
- Neue VM-Intrinsics für Task-local Storage sollen nur eingeführt werden, wenn ein echter Use Case bleibt.
Mit anderen Worten: erst beweisen, dann einbauen. Revolutionär.
14. Scripting Host via .jdm und Hot-Reload¶
Status: Informativ. Noch nicht spec-reif.
Entscheidung¶
Jade braucht keinen separaten Scripting-Layer wie Lua. Die VM selbst ist der Scripting Host. .jdm-Module sind kompiliert, typgeprüft und ladbar.
Grundprinzip:
- Host-Applikation exportiert API als JDL-/JDM-Kontrakt.
- Script-Code importiert diesen Kontrakt.
- Compiler prüft Scripts statisch.
- VM lädt kompiliertes
.jdmzur Laufzeit. - State kann über Handles erhalten bleiben.
Hot-Reload-Idee¶
jade build --module patrol.jdl -> patrol.jdm
Engine erkennt neue patrol.jdm
-> alte PatrolTask kooperativ stoppen
-> neue PatrolTask starten
-> bestehender Shared[ScriptState]-Handle wird übergeben
JDL-Sicht:
Der State überlebt Reload, weil die Engine den Handle hält. Die Task kann sterben, ohne den State zu vernichten.
Offene Fragen¶
- Wie granular ist Hot-Reload? Funktion, Modul, Plugin?
- Wie werden Script-APIs versioniert?
- Wie erzwingen Package-/Plugin-Constitutions Sandboxing?
- Wie sieht Fehlerreporting im laufenden Host aus?
- Wie werden migrationspflichtige State-Layouts behandelt?
15. Offene Punkte¶
Diese Punkte gehören nicht mehr als lose Ideensammlung ans Ende, sondern als konkrete TODO-Liste für spätere Konsolidierung:
- Collections normativ konsolidieren
MapLike,MutableMap,PersistentMapArray[T],[T],[T; N]-
Wire-/ABI-Stabilität konkreter Collections
-
MapSpec / Collection Blueprints prüfen
- Wann lohnt Blueprint gegenüber normaler TypeFn?
- Welche Felder sind Strategie, welche sind Policy?
-
Welche Engine materialisiert daraus konkrete Typen?
-
Function Captures und Blueprint-lokale Funktionen
- Inline-Funktionen dürfen nur Parameter, Blueprint-Felder, FQN/importierte Symbole und Context-Services referenzieren.
-
Externe Lexical Captures bleiben verboten oder müssen explizit materialisiert werden.
-
Variance finalisieren
- immutable Container kovariant
- mutable Container invariant
- Funktionsparameter kontravariant
- Funktionsrückgaben kovariant
-
Effect[R, E, D]:Düber Row-Satisfaction, nicht naive Subtyping-Variance -
FFI-Boundary für Buffer/Slices
- keine rohen Pointer in JDL
- explizite FFI-Grenztypen
-
Arena-/Slice-Escape-Regeln
-
=?Lowering in VM/IR - Desugaring auf
match/early return - Cleanup-Semantik bei
Disposableundwith - Diagnosequalität
16. Kurzfassung¶
Die wichtigsten aktualisierten Entscheidungen:
..bleibt normaler Operator über Protocols.Interval[T]ist die bevorzugte kontinuierliche Form.- Parser-Kombinatoren bleiben Bootstrap-Strategie.
- Diagnostics sind strukturiert und proof-/trace-fähig.
RcersetztRef.- Arenen werden über
withundMemory(Arena[A])geführt. - Slices sind Views, Scope-Escape erzeugt Kopie oder Fehler.
- Strings sind UTF-8 und immutable.
- Collections werden als typalgebraische Familien verstanden.
Map[K,V]ist Default, nicht ABI-/Wire-Vertrag.- Blueprints und
&ermöglichen Collection-Spezialisierung. - Newtypes materialisieren Domänen über bestehenden Carriern.
- Tags, Phantoms und zero-field Newtypes bleiben getrennte Werkzeuge.
- Effect-artige Stdlib-Features entstehen aus normalen JDL-Bausteinen.
- Scripting via
.jdmbleibt ein starkes, aber noch zu spezifizierendes Ziel.
Guter Tag für Jade. Schrecklicher Tag für halbherzige Typmodelle.