Zum Inhalt

Addendum — Begriffe, Motive, verworfene Wege

Art: Ergänzung zu wie-jade-uebersetzt und wie-jade-ausfuehrt. Verhältnis: Die beiden Hauptdokumente erklären wie das Modell gebaut ist. Dieses Addendum erklärt warum bestimmte Grenzen so gezogen wurden, welche Alternativen verworfen wurden und welche Fragen ausdrücklich offen bleiben. Quellenbasis: Diese Fassung stützt sich ausschließlich auf die beiden Hauptdokumente und auf die bereits im Addendum festgehaltenen Entscheidungen. Bei einem Widerspruch haben die beiden Hauptdokumente Vorrang. Nutzung: Teil A ist ein Nachschlagewerk. Teil B bis F erklären die wiederkehrenden Motive. Teil G hält verworfene Wege fest. Teil H grenzt den noch offenen Entwurfsraum ab. Teil I sammelt bewusst außerhalb der Hauptdokumente formulierte Architekturüberlegungen. Teil I ist nicht normativ; er dokumentiert einen konsistenten Denkstand, der erst nach ausdrücklicher Übernahme in die Hauptspezifikation verbindlich wird.


Teil A — Begriffe

Präzise Definitionen für das Vokabular, das die beiden Dokumente benutzen. Wo zwei Begriffe leicht verwechselt werden, steht die Abgrenzung dabei.

A.1 Rund um die Anfrage

Query — Eine Frage an die CompilerDB. Sie hat einen Schlüssel und ist deterministisch: Dieselbe Frage im selben Zustand hat dieselbe Antwort. Es gibt zwei Sorten. Eine Input Query wird gesetzt und nie berechnet; eine Derived Query hat einen Provider.

QueryResult — Der Cache-Record einer beantworteten CompilerDB-Query: kanonisch serialisierte Antwort plus Result-Typ, Revisions-/Durability-Daten, contentHash und optionaler DerivationKey. Bei Einzelwert-Queries enthält data typischerweise ein JadeResult[T] (Ok, Err, Empty). Ein semantischer Fehlschlag kann daher ebenso cachebar sein wie ein Erfolg.

Nicht zu verwechseln mit: dem DerivationRecord. Das QueryResult sagt was der nächste identische Aufrufer erhalten soll; der Record sagt warum diese Ableitung so ausging. Resolved/Failed(...) klassifiziert den DerivationOutcome, nicht den QueryResult-Typ.

Provider — Eine Funktion, die eine Derived Query beantwortet. Sie liest über die db-Fassade, und was sie liest, wird zu Abhängigkeitskanten.

Revision — Ein Zähler, der bei jedem setInput steigt. Er markiert keinen Zeitpunkt, sondern einen Zustand der Eingaben. Ergebnisse tragen zwei Revisionszahlen: wann sie zuletzt geändert und wann sie zuletzt überprüft wurden.

Early Cutoff — Die Regel, dass eine Neuberechnung mit unverändertem Ergebnis nur die Prüfzahl aktualisiert, nicht die Änderungszahl. Damit propagiert die Ungültigkeit nicht weiter.

Durability — Eine Einstufung, wie wahrscheinlich sich ein Input ändert. Der Seed ist das Extrem: nie während eines Laufs.

Cancellation — Der Abbruch einer laufenden Auswertung, weil eine neue Revision sie überholt hat. Sie erzeugt weder ein QueryResult noch einen DerivationRecord — die Frage blieb unbeantwortet.

A.2 Rund um die Ableitung

Derivation — Eine tatsächlich ausgeführte Berechnung einer Query in einer bestimmten Revision. Nicht die Frage, nicht die Antwort, sondern der Vorgang dazwischen.

DerivationKey — Subject/Query + Revision + Origin. Beachte, dass keine DescriptorId darin vorkommt: Eine Ableitung kann scheitern, und dann gäbe es keine.

DerivationRecord — Was bei einer Ableitung entstanden ist:

reads         welche Queries gelesen wurden
patchSet      Option[PatchSet] — nur wenn die Ableitung einen Materialisierungspfad nimmt
outcome       Resolved | Failed(Provider, …) | Failed(Materializer, …)
materialized  Option[DescriptorId] — nur wenn ein Descriptor committet wurde

Provenance — Der Oberbegriff für alles, was die Entstehung erklärt: Proof Trace, Source Spans, Herkunft, Diagnostik-Verweise. Sie geht nie in den contentHash.

Proof Trace — Der lesbare Teil einer Provenance: die Kette, aus der eine Aussage folgt. Grundlage von :why.

Semantic View — Eine typisierte semantische Projektion eines Subjects für eine Analyseachse, technisch als gewöhnliche Derived Query ausgedrückt. ControlFlowView oder EffectView sind mögliche Ergebnisformen. View ist kein Keyword, keine neue Query-Art und kein Ring-0-Typ.

Fact — Im Proof-Vokabular eine semantisch bekannte Aussage, die ein Proof konsumieren kann. Facts stammen aus QueryResults beziehungsweise Semantic Views; temporäre Zwischenschritte einer Analyse sind noch keine kanonischen CompilerDB-Ergebnisse. Fact ist dabei kein neuer CompilerDB-Begriff und ausdrücklich nicht mit den axiomatischen Ring0Fact-Einträgen zu verwechseln.

Proof Obligation — Eine Aussage, die eine konkrete Proof-Regel zusätzlich nachweisen muss. Ihre Erfüllung kann weitere Queries auslösen.

Proof — Eine nachvollziehbare Ableitung einer Aussage aus Facts und erfüllten Obligations. Die semantische Aussage und ihre Provenance bleiben getrennt.

unproven — Im Labelmodell der Zustand „weder bewiesen noch ausgeschlossen“. Allgemeiner gilt nur das Prinzip: Ein fehlender Proof ist kein Gegenbeweis. Andere Proof-Domänen müssen nicht dieselben drei Zustände wie Labels verwenden.

A.3 Rund um die Materialisierung

PatchSet — Eine Änderungsabsicht für einen Materialisierungspfad. Eine Engine beziehungsweise ein materialisierender Provider gibt sie zurück statt eines fertigen Descriptor-Eintrags. Eine direkte Query-Antwort, etwa ein AST des Parsers, braucht kein PatchSet. Ein PatchSet hat keine DescriptorId, keinen Platz im DescriptorStore und keine Gültigkeit, bis der Materializer es annimmt.

Materializer — Die einzige Instanz mit Schreibrecht auf den DescriptorStore. Er prüft Ring-Ebene, Kohärenz, Konflikte und die Constitution, und committet atomar oder gar nicht.

Descriptor — Ein geprüfter, unveränderlicher, kanonisch serialisierbarer Vertrag. Er erfüllt vier Bedingungen gleichzeitig: aus einem Subjekt entstanden, von einer Engine materialisiert, von einer tieferen Schicht konsumiert, nach dem Commit unveränderlich.

Nicht jeder Name auf …Descriptor ist einer. Ein IrModule etwa entsteht nicht aus einem Blueprint über eine Engine — es ist ein Artefakt.

semanticPayload — Der Teil eines Deskriptors, der seine Bedeutung ausmacht: Layout, Felder, Varianten, Policies, Referenzen. Nur er geht in den contentHash.

contentHash — Ein Hash über die kanonische Form des semanticPayload, einschließlich kanonisch ausgedrückter Referenzen und niemals lauf-lokaler Arena-Ids. Er trägt semantische Inhaltsidentität über Compilerläufe hinweg. Der FQN trägt daneben die nominale Identität; die lauf-lokale TypeId tut beides nicht.

Interning — Beim Commit wird der contentHash nachgeschlagen. Gibt es den Inhalt schon, bekommt der Aufrufer die vorhandene Id.

A.4 Rund um die Engines

Engine — Eine Implementierung mit describe und optional instantiate. Sie hält keinen Zustand.

Provider gegen Engine — Ein Provider ist eine Rolle im Query-Mechanismus. Eine Engine ist eine Implementierung. Jede Engine kann im Compilerpfad Provider sein; nicht jeder Provider ist eine Engine. Der Parser ist Provider für GetFileAST und keine DescriptorEngine, weil er nichts materialisiert.

DescriptorEngine — Hat nur describe. Erzeugt Wahrheit, läuft nie zur Laufzeit.

RuntimeEngine — Hat describe und instantiate. Die erste Hälfte läuft beim Übersetzen, die zweite zur Laufzeit.

Die vier Ebenen des Wortes:

EngineDefinition     JDL-Code, hat nie Zustand
EngineDescriptor     geprüfter Vertrag
EngineRuntimeConfig  Ring-0-Daten: Limits, Timer, mechanische Runtime-Parameter
EngineInstance       lebt, hat Zustand, sichtbar über ein Handle

A.5 Rund um die Laufzeit

RuntimeBus — Das Command-Substrat. Gegenstück zur CompilerDB: reihenfolgenabhängig, seiteneffektbehaftet, nie gecacht.

Intrinsic gegen ContextApi — Eine Intrinsic ist der Weg zur Maschine und prüft die konkrete Maschinenoperation. Die ContextApi ist die interne contextgebundene Runtime-Sicht/Fassade davor; sie stellt Bindings, Policies und die für Runtime-Infrastruktur zulässige Context-Sicht bereit. Sie ist weder user-facing noch ein ambienter Service-/Resource-Locator.

Context — Die geprüfte, descriptor-materialisierte Umgebungsgrenze einer Ausführungsdomäne. Statisch ist sein Vertrag, nicht notwendig seine Materialisierung.

Proto gegen Frame — Der Proto ist das kompilierte Funktionsobjekt: unveränderlich, von allen Ausführungen geteilt. Der Frame ist eine Ausführung: Header plus Registerfile, von Call bis Return.

Reified — Ein Typ, dessen Beschreibung ins Artefakt projiziert wird. Was nicht reified ist, hat zur Laufzeit keine abfragbare Beschreibung — nicht weil sie versteckt wäre, sondern weil sie nicht existiert.

dyn P — Eine Position, an der nur das Protokoll P statisch bekannt ist, nicht der konkrete Typ. Kein Typkonstruktor: dyn ist ein Schlüsselwort mit einem Protokoll dahinter. Eine Schreibweise Dyn[P] sollte auf der Sprachebene nicht auftauchen, weil sie aussieht wie ein gewöhnlicher generischer Typ und sich nicht so verhält.

DynValue — Die VM-interne Struktur dahinter: Payload plus persistente Typidentität. Sie ist die einzige Wertekategorie im System, die einen Tag trägt — JadeValue ist ungetaggt und wird vom Bytecode-Kontext interpretiert, und genau das geht nicht, wenn der Kontext den Typ nicht kennt. Ein DynValue ist größer als ein Registerslot und wird deshalb von der Memory Engine auf dem Heap gehalten.

dyn gegen reified — Zwei Aussagen auf verschiedenen Achsen, die leicht verwechselt werden:

dyn P      Aussage über eine Stelle    hier ist der Typ nicht bekannt
reified    Aussage über einen Typ      dieser trägt seine Beschreibung ins Artefakt

Sie zeigen in entgegengesetzte Richtungen: dyn verzichtet an einer Stelle auf Wissen, reified hält Wissen für einen Typ vor. Die Kosten fallen entsprechend verschieden an — dyn pro Wert eine Allokation und ein indirekter Aufruf, reified einmalig Bytes im Artefakt.

Die Implikation läuft nur in eine Richtung: Ein dyn-Wert braucht Laufzeit-Typidentität, also müssen Typen, die dort landen, projiziert sein. Umgekehrt gilt das nicht — ein Typ kann reified sein, weil er serialisiert wird, ohne dass je ein dyn im Spiel ist.

Wer entscheidet ist der unbequeme Teil. reified steht am Typ, also entscheidet sein Autor. dyn steht an der Verwendungsstelle, also entscheidet, wer den Wert hält — und stellt damit eine Anforderung an Typen, die ihm nicht gehören. Siehe H.2.

A.6 Rund um Row, Bindung und Grenzen

Effect Row — Die Menge semantischer Anforderungen, die eine Funktion noch nicht lokal abgetragen hat. Service-Elemente werden durch Bindungen erfüllt; Throw[E] wird durch Fehlerbehandlung eliminiert. Suspendierbarkeit und Capabilities sind keine Row-Elemente.

Service Requirement — Eine semantische Anforderung wie FileSystem oder Db. Sie beschreibt, was Code braucht, nicht welche konkrete Implementierung oder Maschinenautorität dahintersteht.

Binding / provide — Die Zuordnung einer semantischen Anforderung zu einer konkreten Implementierung. An dieser Stelle können mechanische Capability-Anforderungen entstehen.

Capability — Maschinenautorität, die eine konkrete Implementierung benötigt und die der Context autorisiert. Sie steht nicht parallel zum Service in der Row.

Bridge — Der explizite Pfad zwischen isolierten Context-Domänen. Ob die Mechanik lokal, über einen Thread, Prozess oder Node läuft, ändert die semantische Rolle der Bridge nicht.

Nursery — Eine Laufzeitdomäne für strukturierte Task-Ownership. Sie besitzt Tasks, keine anderen Runtime-Ressourcen, und ausschließlich solche derselben Ausführungsdomäne. Ihr Eigentümer kann ein Context sein — die Root-Nursery —, ein Task oder eine langlebige Runtime-Entität. Kein Speicherbereich, keine Isolationsgrenze. Siehe I.16.

Root-Nursery — Die Nursery, die jeder ausführbare Context mitbringt. Sie entsteht mit ihm, endet mit ihm und darf leer sein. Sie ist das Ownership-Ziel für den ersten Task und ein mögliches Ziel für Arbeit, die ausdrücklich an die Lebensdauer des Contexts übergeben wurde — erreichbar ist sie damit nicht, das ist eine eigene Autoritätsfrage.

Control Surface — Die Bedien- und Introspektionsschicht für Kommandos wie :why, :type oder :tasks. Ein Control-Surface-Kommando ist nicht automatisch ein RuntimeBus-Command; der Dispatcher wählt das zuständige Substrat.


Teil B — Warum die CompilerDB so gebaut ist

B.1 Bedarf statt Pipeline

Das Problem: Eine Pipeline weiß nach einer Änderung nicht, was betroffen ist. Also macht sie alles neu. Bei einem einmaligen Übersetzen ist das hinnehmbar; bei einem Sprachserver, der bei jedem Tastendruck übersetzt, nicht.

Die Antwort: Nichts läuft, weil es an der Reihe wäre. Etwas läuft, weil jemand gefragt hat und die Antwort fehlt.

Der Preis: Buchhaltung. Man muss wissen, wovon jede Antwort abhängt, und diese Information muss zuverlässig entstehen — nicht durch Disziplin, sondern durch Mechanik.

Was ohne das passiert: Sprachserver und Compiler müssten ihre Invalidierungslogik getrennt pflegen. Das Modell der Hauptdokumente vermeidet genau diese zweite, parallele Zustandsmaschine.

B.2 Warum Cache und DescriptorStore getrennt sind

Das Problem: Beide halten Ergebnisse, aber mit verschiedenem Anspruch.

Ein Cache-Eintrag ist eine Beschleunigung. Wirf ihn weg, rechne neu, dasselbe Ergebnis — nur langsamer.

Ein Deskriptor ist kanonische, geprüfte Semantik. Solange seine CompilerDB-Instanz lebt, können andere Einträge über seine lokale Id genau auf diese Bedeutung verweisen. Deshalb wird ein Descriptor weder an Ort und Stelle ersetzt noch einzeln verworfen; bei neuer Semantik entsteht ein anderer kanonischer Eintrag.

Was ohne die Trennung passiert: Entweder man behandelt alles wie einen Cache — dann brechen Referenzen — oder alles wie eine Wahrheit, dann wächst der Speicher mit jeder Neuberechnung und man kann nie etwas verwerfen.

B.3 Warum niemand den DepGraph schreibt

Das Problem: Deklarierte Abhängigkeiten sind immer unvollständig. Jemand vergisst eine, und dann invalidiert etwas nicht, und der Fehler tritt eine Woche später bei jemand anderem auf.

Die Antwort: Die Kante entsteht durchs Lesen. Ein Provider deklariert nichts; er stellt Queries, und die CompilerDB schreibt mit.

Die Folge, die man mitkaufen muss: Die Lesesicht eines Providers muss eingeschränkt sein. Was an ihr vorbei gelesen würde — eine Datei, die Uhr, ein globaler Zustand — stünde nicht im Graphen. Deshalb ist ein Provider, der heimlich etwas liest, kein Stilproblem, sondern ein Korrektheitsproblem.

B.4 Warum es Patches gibt

Das Problem: Wenn eine Engine direkt schreiben dürfte und auf halbem Weg scheitert, hinterlässt sie einen halben Zustand. Dann braucht sie Aufräumlogik, und die ist genau der Code, der im Fehlerfall selten getestet wird.

Die Antwort: Sie gibt eine Änderungsabsicht zurück und schreibt nirgends. Ein Abbruch mitten in einer Schleife hinterlässt nichts.

Der Nebeneffekt, der wichtiger ist als der Hauptzweck: Weil das PatchSet ein Wert ist, kann der Materializer es prüfen, bevor irgendetwas gilt. Ring-Ebene, Kohärenz, Konflikte, Constitution — alles an einer Stelle, gegen ein vollständiges Bild.

B.5 Warum der Materializer der einzige Schreiber ist

Das Problem: Bei mehreren Schreibern muss jeder alle Invarianten kennen. Kommt eine dazu, müssen alle nachziehen, und einer vergisst es.

Die Antwort: Eine Stelle, ein Prüfsatz, atomarer Commit.

Was das ermöglicht: Das OnDescriptorCommit-Gate der Constitution. Wenn es viele Schreibwege gäbe, müsste die Verfassung an jedem hängen — oder sie wäre umgehbar.

B.6 Warum Provenance in einem eigenen Speicher liegt

Das Problem: Der contentHash soll semantische Identität ausdrücken. Enthielte er Source-Spans, änderte das Verschieben einer Typdefinition um zehn Zeilen die Identität — und das Interning wäre wertlos.

Die tiefere Version: Zwei Ableitungswege können denselben Inhalt liefern. Läge die Herkunft im Eintrag, gewänne die erste und die zweite verschwände, oder man müsste einen unveränderlichen Eintrag nachträglich ergänzen.

Die Ausrichtung, die man leicht falsch wählt: Provenance hängt nicht am Deskriptor, sondern an der Ableitung. Eine gescheiterte Ableitung hat gar keinen Deskriptor — und ihre Erklärung ist trotzdem wertvoll.

Und die Erlaubnis, die dazugehört: Der ProvenanceStore darf veralten. Ohne sie hätte er die Unveränderlichkeit des DescriptorStore geerbt, einfach weil er danebenliegt, und ein Sprachserver hätte nach einer Stunde tausend Erklärungen pro Ableitungsweg.


B.7 Warum Semantic Views keine neue CompilerDB-Schicht sind

Das Problem: Analysen brauchen dieselbe Semantik in unterschiedlichen Formen. Totalität braucht Kontrollfluss und Fortschritt, Pure braucht Effekte, andere Analysen brauchen Calls oder Ownership. Wenn jede Analyse direkt jeden AST-Kind kennt, koppeln neue Konstrukte und neue Analysen quadratisch aneinander.

Die Antwort: Eine View soll nur ein gut geschnittener Derived-Query-Vertrag sein. Die CompilerDB braucht dafür weder ViewId noch eine zweite Registry noch eine neue Result-Kategorie. Sie kann bereits Queries auswerten, Provider auswählen, Ergebnisse cachen und Reads tracken. Noch nicht vollständig gelöst ist dagegen die allgemeine Query-Erweiterbarkeit: Die aktuelle Spec bindet neue Query-Arten an hostseitige Query-Structs/TypeTag. Wenn neue View-Verträge selbst in JDL entstehen sollen, muss diese allgemeine Query-Vertragsregistrierung datengetrieben werden. Das ist eine Lücke der CompilerDB-Self-Hosting-Grenze, nicht eine fehlende View-Schicht.

Subject
  -> GetControlFlowView
  -> ControlFlowView
  -> TotalityProvider / Reachability / DeadCode / ...

Warum das für Self-Hosting zählt: Der Zig-Kern besitzt die Mechanik, konkrete Views aber JDL. Ein Stage-1-BootstrapProvider darf denselben Query-Vertrag vorübergehend beantworten; wenn Stage 2 auf einen JDL-Provider umschaltet, ändert sich der Vertrag nicht.

Die Provider-Eindeutigkeit: Neue Konstrukte dürfen nicht je einen konkurrierenden Provider für denselben View-Query-Vertrag registrieren. Der eindeutige Ring-1-Provider muss konstrukt-spezifische Projektionen über normale JDL-Semantik delegieren, etwa über provide/Dispatch. Welche konkrete Protokollform das erhält, ist ein API-Entscheid und keine CompilerDB-Erweiterung.

Die Grenze: Eine View soll eine wiederverwendbare Analyseachse sein, keine zweite AST-Klasse. Ein View-Query pro Kante, Feldzugriff oder Mini-Fakt wäre mechanisch möglich und architektonisch dumm: Query-Dispatch und Dependency-Buchhaltung würden die eigentliche Analyse auffressen.

B.8 Warum Proofs nicht in den Konstrukten wohnen

Das Problem: Wenn jedes Konstrukt Methoden wie provePure, proveTotal, proveSync und jede spätere Analyse selbst tragen müsste, würde jede neue Proof-Art Änderungen quer durch alle Konstrukte erzwingen.

Die Antwort: Konstrukte liefern Facts beziehungsweise Semantic Views. Proof-Regeln konsumieren sie und erzeugen Obligations. Damit wächst eine Achse unabhängig von der anderen.

neues Konstrukt -> bestehende Views -> bestehende Proofs
neuer Proof     -> bestehende Views -> keine AST-Umschreibung

Der zuständige CompilerDB-QueryProvider bleibt eindeutig. Mehrere logische Proof-Regeln sind interne JDL-Komposition oder getrennte Unterqueries, keine konkurrierenden Provider für denselben QueryKey.

B.9 Warum ein lokaler Fixpunkt kein CompilerDB-Fixpunkt ist

Für eine Analyse, deren Fachvertrag eine monotone Closure vorsieht, ist ein Fixpunkt über eine endliche Faktendomäne ein normaler Algorithmus. Die Label-Inferenz ist das konkrete Beispiel. Eine solche Worklist kann innerhalb eines Meta-Providers laufen und terminieren, sobald keine Regel mehr einen neuen Fakt erzeugt.

Das ist etwas anderes als eine Query-Rekursion A -> B -> A. CompilerDB-Zyklen bleiben Fehler. Die Datenbank verfolgt nur die externen Reads des Providers; Worklist, SCC-Zerlegung und temporäre Faktensätze sind lokale Berechnungszustände.

Damit bleiben zwei Zusagen gleichzeitig wahr:

Meta kennt keinen globalen "solange alles wiederholen"-Compilerpass.
Eine einzelne totale Meta-Berechnung darf intern einen endlichen Fixpunktalgorithmus ausführen.

Teil C — Warum Deskriptoren so aussehen

C.1 Warum es ein Kriterium braucht

Das Problem: Es existieren rund sechsundsechzig verschiedene Namen auf …Descriptor, davon achtunddreißig in genau einem Dokument. Das ist keine Taxonomie, sondern eine Benennungsgewohnheit.

Die Antwort: Vier Bedingungen, alle gleichzeitig. Wer eine reißt, ist etwas anderes — ein Query-Ergebnis, ein Konfigurationsrecord, ein Profil oder ein Domänenobjekt.

Wofür das gut ist: Nicht zum Aufräumen um des Aufräumens willen. Es entscheidet, was in eine geschlossene, normative Kernmenge gehört und was in eine offene Domänenmenge, die mit jedem Plugin wachsen darf.

C.2 Warum es drei Identitätsformen gibt

TypeId        lokale Identität: Arena-Index innerhalb einer CompilerDB-Instanz
contentHash   semantische Identität: kanonischer Inhalt über Läufe hinweg
FQN           nominale Identität: stabiler vollqualifizierter Name

Das Problem: Die drei Fragen sind verschieden: Wo liegt der Eintrag in dieser CompilerDB-Instanz? Ist seine kanonische Semantik gleich? Unter welchem nominalen Namen ist er bekannt? Eine einzige Identitätsform beantwortet diese Fragen nicht zugleich.

Die Antwort: Alle drei, mit getrennten Zuständigkeiten. Innerhalb einer CompilerDB-Instanz adressiert man über die TypeId. Über Laufgrenzen trägt sie nicht; dafür dienen contentHash und FQN mit unterschiedlicher Bedeutung.

Wo das zuerst wehtut: Beim Seed. Werden TypeIds in Anlagereihenfolge vergeben und der Seed in einer anderen Reihenfolge gebaut, bekommt dasselbe Programm andere Ids — und jeder persistente Cache verfehlt.

C.3 Warum die Projektion einseitig ist

Das Problem: Ein Artefakt geht an fremde Rechner. Was darin steht, ist nicht mehr kontrollierbar.

Die Antwort: Nur, was zur Ausführung gebraucht wird. Meta-Records, Proof Traces, AST, Spans, Constitution-Regeln bleiben zurück.

Die Zusage dahinter: Aus einem Artefakt lässt sich nicht rekonstruieren, wie das Programm entstanden ist.

Und die Folge, die der eigentliche Punkt ist: Zur Laufzeit läuft keine TypeEngine — nicht weil man sie abgeschaltet hätte, sondern weil das Material fehlt. Ein nicht-reified Typ hat nichts, was man abfragen könnte.

C.4 Warum Interning nicht optional ist

Das Problem: Ein append-only Store, in den jede Neuberechnung schreibt, wächst mit der Zahl der Berechnungen.

Die Antwort: Unveränderliche Einträge mit Inhaltshash internen sich von selbst. In einem Sprachserver mit tausend Revisionen entstehen kaum neue Deskriptoren, weil dieselbe Typform immer wieder zum selben Inhalt materialisiert.

Was es nicht leistet: Wachstum durch tatsächlich neue Semantik. Der Store wächst weiter, nur eben nicht durch Wiederholung.

Der geschenkte Nebeneffekt: Option[i32] und Result[i32, E] teilen sich womöglich denselben LayoutDescriptor, ohne dass es jemand plant.


Teil D — Warum Engines so gebaut sind

D.1 Warum sie Provider sind statt Dienste

Das Problem: Ein Dienst, den man aufruft, läuft, wann man ihn aufruft. Dann muss der Aufrufer wissen, ob das Ergebnis noch gilt.

Die Antwort: Die Frage geht an die Datenbank. Ob die Engine dafür läuft, entscheidet der Cache. Der Normalfall ist, dass sie nicht läuft.

Der Effekt, den man dabei geschenkt bekommt: Inkrementalität ist keine Eigenschaft der Engine, sondern des Mechanismus. Eine Engine, die nichts über Caching weiß, ist trotzdem cachebar.

D.2 Warum sie keinen Zustand haben

Das Problem: Zustand in einer Compilerkomponente ist Zustand, der zwischen Läufen falsch sein kann.

Die Antwort: Sie existiert zwischen zwei Aufrufen nicht. Alles, was sie braucht, liest sie; alles, was sie produziert, gibt sie zurück.

Die Verbotsliste ist deshalb keine Regel, sondern eine Beobachtung: Eine EngineDefinition hat gar keinen Ort für Scheduler-Queues, Handle-Tabellen oder JME-Speicher.

D.3 Warum Rekursion über die Datenbank läuft

Das Problem: Ein Struct mit Struct-Feld braucht dieselbe Auskunft für den inneren Typ. Der naheliegende Weg wäre ein rekursiver Aufruf.

Was dabei kaputtgeht, dreifach:

keine Abhängigkeitskante   der innere Typ ändert sich, nichts invalidiert
kein Cache                 dasselbe Feld zweimal, zweimal gerechnet
kein Zyklusschutz          ein zyklischer Typ füllt den Stack

Die Antwort: db.requireProvide(…) statt encodeStruct(…). Alle drei Probleme lösen sich, ohne dass jemand daran denkt.

D.4 Warum es zwei Protokolle gibt

Das Problem: Manche Engines beschreiben nur, manche betreiben auch etwas. Ein gemeinsames Protokoll hätte für die erste Sorte eine Methode, die nie aufgerufen wird.

Die Antwort: DescriptorEngine hat describe, RuntimeEngine hat beides. Es gibt keine Engine mit nur instantiate — sie hätte nichts, worauf sie sich stützt, und müsste zur Laufzeit ableiten, was der Compiler hätte prüfen sollen.


Teil E — Warum die Laufzeit so getrennt ist

E.1 Warum zwei Substrate

CompilerDb   Queries    deterministisch, cachebar
RuntimeBus   Commands   reihenfolgenabhängig, nie gecacht

Das Problem: Eine Query darf man wiederholen, ein Command nicht. Läge beides auf einem Weg, müsste jeder Aufrufer wissen, welche Sorte er gerade auslöst.

Und die Richtung ist asymmetrisch: Kein Laufzeitzustand kommt implizit in die CompilerDB zurück. Sonst wäre eine Query nicht mehr deterministisch. Laufzeitdaten können nur über eine ausdrückliche Input-Grenze zu neuen Compilereingaben werden — damit bleibt der Weg für Profildaten offen, ohne das Modell zu brechen.

E.2 Warum es kein Kabel zwischen ihnen gibt

Das Problem: Was hinüberginge, überlebt den Transport nicht. Eine TypeId ist ein Arena-Index. Ein Handle ist lauf-lokal. Ein Proof Trace verweist auf Quelldateien, die auf der Zielmaschine nicht existieren.

Die Antwort: Ein Bruch, kein Kanal. Dazwischen liegt das Artefakt, und der Übergang ist eine Filterung mit Umschreibung auf stabile Identitäten.

E.3 Warum ein Context mehr als ein Handler-Satz ist

Das Problem: Die Effect Row beschreibt semantische Anforderungen einer Berechnung. Würde man den Context nur als Endpunkt dieser Anforderungen verstehen, fehlten genau die anderen Dinge, die eine Ausführungsdomäne bestimmen: Resource-Bindings, Storage-Rollen, Scheduler-Governance, Limits und Runtime-Policies.

Die Antwort: Ein Context ist die geprüfte, descriptor-materialisierte Umgebungsgrenze einer Ausführungsdomäne. Er bindet Services, Resources und Runtime-Policies und definiert explizite Beziehungen zu anderen Contexts. Service-Requirements werden gegen seine Bindungen aufgelöst, aber der Context ist weder bloß ein Handler-Satz noch ein Service-Locator.

Was dadurch der leere Pfeil von main bedeutet: Nicht „dieses Programm benutzt nichts", sondern „an der äußeren Grenze existiert keine unbezahlte semantische Schuld mehr". Das ist eine Eigenschaft der Row; der Context besitzt darüber hinaus die übrige Runtime-Umgebung.

E.4 Warum Maschinenautorität nicht in der Row steht

Das Problem: Stünde konkrete Maschinenautorität neben jedem Service in der Row, entstünde parallele Buchhaltung durch jede Aufrufkette, obwohl der Aufrufer nur die semantische Anforderung kennt.

->{FileSystem, FsReadAuthority}   Doppelbuchhaltung
->{FileSystem}                    was gemeint ist

Die Antwort: Die Row nennt den Service. Die konkrete Implementierung arbeitet über Resources beziehungsweise ResourceHandles, die ihr legitime Runtime-Autorität geben. Diese Autorität wird beim Provisionieren, Ableiten oder Transferieren von Resources begrenzt; ein globales CapabilitySet oder eine eigene CapabilityEngine ist im aktuellen Modell nicht erforderlich.

Der Restfall: Wird eine Bindung erst zur Laufzeit konkret, bleibt auch dort die Resource-/Context-Grenze maßgeblich. Die ContextApi stellt Runtime-Infrastruktur die geprüfte contextgebundene Sicht bereit, mintet aber keine Autorität und ist kein ambienter Locator für Anwendungscode.

E.5 Warum Funktionen transport-agnostisch sind

Das Problem: Wenn eine Signatur verrät, ob ein Dienst lokal oder entfernt gebunden ist, ändert sich der Code, sobald sich die Topologie ändert.

Die Antwort: Eine Funktion sieht nicht, wie ihre Services gebunden sind. Weder über ein Row-Element noch über eine Abfrage im Rumpf.

Der Preis: Ein Aufruf, der aussieht wie jeder andere, kann je nach Bindung erheblich andere Laufzeitkosten haben. Sichtbar wird das über Werkzeuge und Verfassungsregeln, nicht über eine mechanische Markierung in jeder Signatur — beides kann die Bindung lesen, ohne den aufrufenden Code an die Topologie zu koppeln.

Die Grenze: Die Agnostik reicht so weit wie die VM-Autorität. Bei FFI entscheidet die Wahl, ob ein Absturz die VM mitnimmt, und das ist kein Performanceunterschied, sondern das Vertrauensmodell.

E.6 Warum Suspendierbarkeit nicht in der Row steht

Das Problem: Ein Async-Effekt würde jede Funktion einfärben, die etwas aufruft, das anhalten kann. Das ist die Funktionsfarbe, die man aus anderen Sprachen kennt.

Die Antwort: Zwei verschiedene Fragen an zwei verschiedenen Stellen. Die Effect Row beschreibt semantische Anforderungen; der Scheduler entscheidet, wann eine Berechnung weiterlaufen kann.

Die Kehrseite, ehrlich: Man kann einer Signatur nicht ansehen, ob ein Aufruf anhalten kann. Wo das gebraucht wird — etwa bei einem Callback zwischen fremden Stackframes —, braucht es einen anderen Mechanismus, und den gibt es noch nicht.


Teil F — Die Regeln, die mehrfach auftauchen

Diese vier ziehen sich durch beide Dokumente. Wer sie kennt, kann die meisten Einzelentscheidungen ableiten, statt sie nachzuschlagen.

F.1 Beschreiben, prüfen, materialisieren

Wenn kanonische Semantik für eine tiefere Schicht entsteht, folgt sie dem Materialisierungspfad:

Blueprint oder Subjekt
    ↓
Engine / materialisierender Provider
    ↓
PatchSet
    ↓
Materializer prüft und committet
    ↓
Descriptor
    ↓
tiefere Schicht konsumiert

Das ist kein Universalweg für jedes Compilerergebnis. Direkte Query-Ergebnisse wie ein AST laufen daneben über den allgemeineren Pfad:

Query
    ↓
Provider
    ↓
QueryResult

Damit bleibt die Regel präzise: Materialisierte Semantik entsteht über Prüfung und Commit; nicht jedes Wissen der CompilerDB ist materialisierte Semantik.

F.2 Beschreibung gegen Lebendes

geprüfte Beschreibung was daraus lebt
TypeDescriptor Wert
EngineDescriptor EngineInstance
ContextDescriptor laufende Domäne
Proto Frame

Das Linke ist geprüft, unveränderlich und geteilt. Das Rechte lebt, hat Zustand und gehört einer Ring-0-Domäne. Die rechte Seite leitet nie etwas ab.

Eine Beschreibung wird einmal geprüft und materialisiert; die lebende Seite darf danach nur konsumieren und ausführen.

F.3 Bedeutung gegen Erklärung

Bedeutung           ≠ Erklärung ihrer Entstehung
Descriptor          ≠ Provenance
Runtime-Semantik    ≠ Compilerwissen
Control Surface     ≠ RuntimeBus
Artefakt            ≠ CompilerDB

Was zur Ausführung gebraucht wird, darf nicht davon abhängen, was zur Erklärung gebraucht wird.

Das ist keine Wertung. Provenance ist für Diagnose, Werkzeuge und Vertrauen oft wertvoller als manches Laufzeitartefakt. Sie ist nur nicht ausführungsnotwendig.

F.4 Prüfung hängt am semantischen Vertrag, nicht an einem globalen Zeitpunkt

Die Hauptdokumente kennen keinen universellen Moment, nach dem „alles übersetzt" wäre: Sprachserver und REPL können Compiler- und Laufzeitdomäne dauerhaft nebeneinander halten. Deshalb muss die Zusage an der jeweils materialisierten Beschreibung hängen, nicht an einer globalen Compile-Phase.

Keine Laufzeitinstanz entsteht unter einer Typ-, Engine- oder Context-Semantik, die nicht zuvor geprüft und materialisiert wurde.

Das ist dieselbe Form wie in F.2: Die lebende Seite konsumiert einen bereits geprüften Vertrag und leitet dessen Bedeutung nicht erneut ab.


F.5 Fact gegen Beweis gegen Erklärung

Proof-Fact  = semantisch bekannte Aussage aus kanonisch abfragbarer Information
Proof       = gültige Ableitung aus Facts und Obligations
Provenance  = Erklärung, wie genau diese Ableitung zustande kam

Dieselbe semantische Aussage kann mehrere Proof-Wege tragen. Zwei Proof-Wege dürfen dieselbe semantische Aussage erzeugen, ohne deren Identität zu vervielfachen. unproven hat dagegen keinen erfundenen Gegenbeweis als Provenance: Es bezeichnet nur das ausgeschöpfte Fehlen eines positiven Beweises im zuständigen Regelraum.

Teil G — Verworfene Wege

Was erwogen und aus benennbaren Gründen nicht genommen wurde. Ohne diese Liste wirken manche Entscheidungen willkürlicher, als sie sind.

G.1 Der Seed in JDL

Erwogen: Der Bootstrap-Kern als in JDL geschriebenes, vorkompiliertes Artefakt.

Verworfen, weil: Meta-Werte JME-Werte sind, also beim Übersetzen eine VM läuft. Ein in JDL geschriebener Seed bräuchte einen Compiler, um zu entstehen — und der bräuchte ihn.

Was der Versuch trotzdem gebracht hat: Beim Schreiben des JDL-Entwurfs kamen vier Löcher zutage — Never fehlt in der Primitivenliste, Unit hat keine passende Klasse, usize widerspricht der Determinismuszusage, str hat keine Spezifikation. Diese Funde stammen aus der Übung, nicht aus dem Artefakt.

G.2 Ein zweistufiger Bootstrap für den Meta-Context

Erwogen: Ein minimaler, in Zig verdrahteter Meta-Context, gerade groß genug, um den eigentlichen aus einem Blueprint zu materialisieren.

Verworfen, weil: Er bei jedem Start durchlaufen werden müsste, für eine Umgebung, die sich nie ändert. Und weil zwei Meta-Contexts für einen Moment nebeneinander existierten, mit zwei Antworten auf die Frage, welcher gerade gilt.

Stattdessen: Der Meta-Context ist ein Seed-Eintrag. Einmal in JDL beschrieben, beim Prägen materialisiert, danach nur noch gelesen.

G.3 Sichtbarkeits-Keywords

Erwogen: pub und package als zusätzliche Sichtbarkeitsmechanismen behalten.

Verworfen, weil: Vier Mechanismen — pub, package, internal, mod.jdl-Re-Exports — dieselbe Frage beantworteten und die Oberfläche eines Packages über zwanzig Dateien verstreut war.

Der Preis, der bewusst gezahlt wird: Wer eine Datei aufschlägt, sieht nicht, ob eine Definition freigegeben ist. Der Ausgleich liegt beim Compiler: Er meldet, wenn sich der Fingerabdruck der Oberfläche ändert. Ein Keyword steht nur da und warnt niemanden.

G.4 Ein @compiler-Namensraum

Erwogen: Ein compiler-provided Namensraum für Ziel-, Build- und Layoutfakten.

Verworfen, weil: Er keinen Nutzer gehabt hätte. @cfg wird vor der Namensauflösung ausgewertet und kann einen Namensraum nicht benutzen; wer die Fakten danach braucht, ist in der Meta-Phase und hat db.target().

Der Nebengewinn: @ bedeutet jetzt nur noch eine Sache — Annotation.

G.5 Capabilities als Row-Elemente

Erwogen: ->{FileSystem, FsReadCapability} — die Maschinenautorität sichtbar in der Signatur.

Verworfen, weil: Zu jedem Service eine parallele Capability gekommen wäre, propagiert durch jede Aufrufkette, obwohl sie den Aufrufer nichts angeht.

G.6 Ein Crossing-Element in der Row

Erwogen: Ein Row-Eintrag, der anzeigt, dass ein Aufruf eine Contextgrenze überschreitet — damit die Kosten sichtbar sind.

Verworfen, weil: Es dieselbe Doppelbuchhaltung gewesen wäre wie G.5. Ob ein Service über eine Bridge gebunden ist, entscheidet der Context, nicht der Code. Und stünde die Mechanik im Element, änderte jeder Umzug eines Dienstes die Signaturen der ganzen Aufrufkette — womit die Bridge-Abstraktion ihren Zweck verlöre.

G.7 Provenance als Feld des Deskriptors

Erwogen: Herkunft, Spans und Proof Trace als Teil des DescriptorEntry.

Verworfen, weil: Zwei Ableitungswege denselben Inhalt liefern können. Dann gewänne die erste Herkunft und die zweite verschwände — oder man müsste einen unveränderlichen Eintrag nachträglich ergänzen.

Und weil: Eine gescheiterte Ableitung hat gar keinen Deskriptor, an dem sie hängen könnte. Ihre Erklärung ist trotzdem wertvoll.

G.8 Einzelne Cache-Einträge freigeben

Erwogen: Eine Freiliste oder ein allgemeiner Allokator für verwaiste Query-Ergebnisse.

Verworfen, weil: Es die Eigenschaft kostet, wegen der Arenen hier richtig sind — Anlegen ist ein Zeigerinkrement, und niemand denkt über Fragmentierung nach.

Stattdessen: Zwei Plätze fester Größe pro Eintrag, Ausweichen in die Arena für Ausreißer. Möglich nur, weil Query-Ergebnisse keine fremden Zeiger enthalten und kanonisch serialisierbar sind.

G.9 Den Parser zur Engine erklären

Erwogen: Eine ParserEngine neben TypeEngine und CodecEngine.

Verworfen, weil: Eine Engine ein PatchSet liefert und etwas materialisiert. Ein Parser liefert ein AST als Query-Ergebnis und materialisiert nichts.

Die Folge, die daran hing: Der Ausgang einer Ableitung durfte danach nicht mehr Resolved(DescriptorId) heißen. Ein erfolgreicher Parser-Provider ist Resolved ohne Deskriptor — und damit ist der Deskriptor endgültig eine optionale Materialisierung statt das Zentrum.


G.10 Views als Ring-0-Primitive

Erwogen: ViewId, eigene View-Registry und spezielle CompilerDB-Operationen in Zig.

Verworfen, weil: Eine View keine eigene CompilerDB-Kategorie braucht. Eine neue View-Schicht würde Mechanik verdoppeln und ausgerechnet Self-Hosting von View-spezifischer Host-Semantik abhängig machen.

Stattdessen: View bleibt Architekturbegriff. Konkrete Views sollen gewöhnliche typisierte Query-Verträge sein. Damit diese Verträge selbst ohne Hostcode hinzukommen können, muss die allgemeine CompilerDB-Query-Vertragsregistrierung noch von der heutigen hostseitigen TypeTag-Form gelöst werden.

G.11 Proof-Methoden an jedem AST-Konstrukt

Erwogen: Jedes Konstrukt implementiert selbst provePure, proveTotal, proveDeterministic und weitere Analysen.

Verworfen, weil: Neue Analysen würden Änderungen an allen Konstrukten verlangen; neue Konstrukte müssten jede bestehende Analyse kennen. Das ist dieselbe Kopplung in zwei Richtungen.

Stattdessen: Konstrukte liefern semantische Views. Proof-Regeln konsumieren diese Views.

G.12 Closure durch rekursive CompilerDB-Queries

Erwogen: Eine Label-Closure ruft dieselben Proof-Queries rekursiv auf, bis der Query-Graph „von selbst“ stabil wird.

Verworfen, weil: CompilerDB-Query-Zyklen absichtlich Fehler sind. Cachebarkeit und eindeutige QueryResult-Semantik würden sonst von einem versteckten globalen Solver abhängen.

Stattdessen: Endliche monotone Closures laufen als lokaler Worklist-/SCC-Algorithmus innerhalb des zuständigen Providers. Externe Fakten werden normal über db.* gelesen.

Teil H — Was dieses Modell nicht beantwortet

Damit das Addendum nicht mehr Geschlossenheit suggeriert, als die beiden Hauptdokumente tatsächlich liefern. Die folgenden Punkte sind dort ausdrücklich offen oder noch nicht vollständig festgelegt.

H.1 Compiler, Identität und Provenance

Kanonische Referenzen im contentHash. Der semanticPayload darf keine lauf-lokalen Arena-Ids hashen. Offen ist, ob eine Referenz über FQN, den Inhaltshash des Ziels oder eine Kombination ausgedrückt wird. Zyklische Typgraphen machen diese Entscheidung nicht trivial.

Relokierbare Referenzen im Artefakt. Im DescriptorStore kann eine Referenz ein lokaler Index sein; im Artefakt darf sie das nicht bleiben. Die konkrete Repräsentation und die Abbildung beim Laden sind noch offen.

Optionale Felder und kanonische Form. Für einen stabilen contentHash muss festgelegt werden, wann ein fehlendes Feld semantisch dasselbe ist wie ein vorhandenes leeres Feld.

Version gegen Revision. Die Dokumente unterscheiden Revisionen des Compilerzustands und neue semantische Descriptorzustände, ohne die endgültige Regel für ihr Zusammenspiel festzulegen.

Der Query-Katalog. Die Beispiele benötigen unter anderem eine referenzliefernde Form wie db.requireProvide, während der bestehende Katalog an dieser Stelle noch nicht vollständig beschrieben ist.

Der Zuschnitt des ProvenanceStore. Welche Ableitungen wie lange aufbewahrt werden, wie aggressiv verdichtet oder verworfen wird und ob erfolgreiche und fehlgeschlagene Ableitungen unterschiedlich behandelt werden, bleibt Implementierungspolitik. Fest steht nur, dass Provenance die Ausführung nicht tragen darf.

Die Deskriptor-Taxonomie. Die Kernmenge der tatsächlich kanonischen Descriptorarten ist noch nicht abschließend von bloßen Records, Profilen und Artefakten abgegrenzt.

H.2 Runtime und Grenzen

Das Speichermodell. Sobald mehrere Tasks denselben Wert beobachten oder verändern können, muss feststehen, welche Ordnungs- und Sichtbarkeitsgarantien gelten und was der Generator über atomare Operationen hinweg umordnen darf.

Suspendierbarkeit an Grenzen. Suspendierbarkeit ist bewusst kein Row-Element. Für Stellen, an denen sie mechanisch bekannt sein muss — insbesondere Callbacks oder fremde Stackgrenzen — fehlt noch der konkrete Mechanismus.

Cross-Context-Handles. Ohne Bridge gibt es keinen direkten Pfad. Noch offen ist, welche Transferform in welchem Fall gilt: Kopie, Lease, Transfer oder ein geteiltes beziehungsweise proxied Bridge-Handle.

Callback-Eintritt. Wenn fremder Code nach JDL zurückruft, muss festgelegt werden, welchen Context, welche Resource-Autorität/Bindings und welchen Task dieser Eintritt erhält.

Shutdown am Context-Ende. Dass eine Nursery ihren Context nicht überleben darf, ist geklärt. Wie das Ende aussieht — geordnetes Drain, Deadline, anschließende Cancellation —, ist es nicht. Die Antwort ist vermutlich nicht eine: Eine Anfrage mit Zeitlimit, ein Plugin-Unload und ein Anwendungs-Shutdown haben verschiedene Fristen.

Übergabe an eine längere Lebensdauer. Dass Arbeit ihre Ownership-Domäne nur durch ausdrückliche Übernahme verlassen darf, steht fest. Das Ziel muss eine autorisierte Nursery mit ausreichend langer Lebensdauer sein; die Root-Nursery eines länger lebenden Contexts ist der allgemeine Fall, nicht der einzige. Offen ist die Form der Autorität — ein TaskSpawner für eine bestimmte Lebensdauer, ein Handle oder etwas anderes. Ausgeschlossen ist nur ParentExpose.

Scheduler-Zusagen statt nur Grenzen. Mehrere Contexts teilen sich einen Scheduler und bringen jeweils ihre eigene Konfiguration mit. Eine Obergrenze wie maxParallelism verhindert, dass ein Context zu viel nimmt — sie sichert ihm nichts zu. Ob es Untergrenzen gibt und in welcher Form (Reservierung, Gewicht, Fairness-Klasse), ist offen. Der Ort steht fest: der ContextDescriptor, nicht die Scheduler-Implementierung.

Lifecycle-Owner einer EngineInstance. Eine Nursery besitzt sie nicht (§I.16). Wer stattdessen — Context, Resource-Scope, Plugin-Lebensdauer oder eine übertragene Ownership —, hängt am Fall und ist nicht allgemein festgelegt.

Wer Reification auslöst. Ein dyn P verlangt Laufzeit-Typidentität von jedem Typ, der dort landet. Ob die Reification dann automatisch aus der Verwendung folgt — dann entscheidet eine fremde Signatur über die Artefaktgröße eines fremden Typs — oder deklariert sein muss — dann bekommt jemand einen Fehler, weil sein Typ hinter einem dyn landet, das er nicht geschrieben hat —, ist offen. Beides ist vertretbar, beides hat Folgen.

:why ohne Provenance-Quelle. Dass die Anfrage aus einem normalen Laufzeitartefakt allein nicht beantwortbar sein kann, ist geklärt. Das konkrete Verhalten des Werkzeugs in diesem Fall ist noch offen.

H.3 Empirische Fragen

Die Zahlen. Wie viele Queries eine typische Datei auslöst, wie oft Early Cutoff greift, wie groß Provenance tatsächlich wird und was Meta-Auswertung kostet, ist noch nicht gemessen. Diese Werte entscheiden nicht über die Semantik, wohl aber darüber, ob die gewählten Implementierungsstrategien praktisch tragen.


Teil I — Bewusst außerhalb formulierte Architekturüberlegungen

Dieser Teil sammelt Überlegungen, die absichtlich nicht als bereits beschlossene Spezifikation formuliert wurden. Sie entstanden aus der Betrachtung der vorhandenen Architektur von außen und sollen als zusammenhängender Denkstand erhalten bleiben.

Die Regeln in Teil A bis H werden dadurch nicht stillschweigend geändert. Wo Teil I eine Umbenennung, Verallgemeinerung oder mögliche neue Grenze beschreibt, ist sie als Kandidat für eine spätere normative Übernahme zu lesen.

I.1 Namenskonvention für Jade-Subsysteme

Die nackten Kürzel JME, JTE, JVM und JRF sind technisch knapp, wirken isoliert aber unnötig stark wie Begriffe aus dem Java-Umfeld. Für öffentliche Systemnamen bietet sich deshalb ein durchgängiges Jade-Präfix an:

JadeME   Jade Memory Engine
JadeTE   Jade Type Engine
JadeVM   Jade Virtual Machine
JadeRF   Jade Runtime Foundation
JadeOS   Jade Operating System

JDL bleibt davon bewusst ausgenommen. Es bezeichnet die Sprache selbst und zugleich ihre Dateiendung:

JDL
*.jdl

Die daraus ableitbare Namensregel lautet:

Öffentliche Jade-Subsysteme tragen Jade als Präfix; JDL bleibt als eigenständiger Sprachname kurz.

Das Präfix ist dabei nicht bloß Branding. Es macht im Text und in Architekturdiagrammen sichtbar, dass ein Begriff ein privilegiertes Jade-Subsystem bezeichnet.

Systemname gegen Namespace

Der öffentliche Systemname muss nicht mit dem sprachlichen Namespace identisch sein. Die Systemnamen dürfen deutlich markieren, dass es sich um Jade-Infrastruktur handelt, während Namespaces knapp und semantisch bleiben:

öffentlicher Systemname     sprachlicher Namespace
JadeVM                      jade::vm
JadeME                      jade::memory
JadeTE                      jade::type
JadeRF                      jade::runtime

Innerhalb eines Namespace sollen Typnamen ihre eigentliche Bedeutung tragen, nicht erneut das gesamte Subsystem präfixieren:

jade::memory::Handle
jade::memory::Arena
jade::vm::Frame
jade::vm::Proto
jade::type::Descriptor

Nicht angestrebt wird eine flächendeckende Qt-artige Präfixierung wie:

JadeMemoryHandle
JadeMemoryArena
JadeVMFrame
JadeTypeDescriptor

Die sinnvolle Lehre aus Qt ist die visuelle Gruppierbarkeit, nicht das 1:1-Übernehmen eines Präfixes für jeden Typ.

Subsysteme tragen die Marke. Typen tragen die Semantik. Namespaces tragen die Struktur.

I.2 Importstil und sichtbare Herkunft

Für JDL bietet sich eine gestufte Nutzung von FQNs und Imports an.

Ein seltener Typ kann vollständig qualifiziert bleiben:

def load(path: jdl::fs::Path) ->{jdl::fs::FileSystem} jdl::io::Bytes

Ein häufig verwendeter Einzeltyp kann direkt importiert werden:

import jdl::fs::Path
import jdl::io::Bytes

def load(path: Path) ->{jdl::fs::FileSystem} Bytes

Als besonders lesbare Standardform bietet sich aber ein Namespace-Import an:

import jdl::fs
import jdl::io

def load(path: fs::Path) ->{fs::FileSystem} io::Bytes

import jdl::fs wäre dabei nicht als Wildcard-Import aller Symbole zu verstehen, sondern als lokale Bindung des Namespace:

jdl::fs        → fs
jdl::fs::Path  → fs::Path

Das hält die Herkunft sichtbar, ohne jeden Typ mit seinem vollständigen FQN auszuschreiben.

Für Kollisionen wäre ein Alias natürlich anschlussfähig:

import foo::storage as foo_storage
import bar::storage as bar_storage

Daraus ergibt sich als mögliche Stilregel:

FQN inline
    wenn ein Typ selten vorkommt oder seine vollständige Herkunft wichtig ist

Namespace-Import
    als bevorzugter Normalfall für zusammengehörige Domänen

Symbolimport
    wenn ein Typ zum lokalen Vokabular des Moduls gehört und sehr häufig vorkommt

Der Namespace übernimmt damit einen Teil der visuellen Gruppierung, für die andere Frameworks Typpräfixe benötigen.

I.3 JadeRF als mechanische Laufzeitbasis

Die Bezeichnung Jade Runtime Foundation (JadeRF) bietet sich für die minimale privilegierte mechanische Schicht an, die materialisierte JDL-Semantik ausführt.

Eine mögliche Grobstruktur ist:

Jade Platform
├── semantische Schicht
│   ├── jade::
│   └── jdl::
│
└── JadeRF
    ├── JadeVM
    ├── JadeME
    ├── Scheduler
    ├── RuntimeBus
    ├── Loader
    ├── Host-Anbindung
    └── Backend-Adapter
        ├── JBC
        ├── WASM
        └── später weitere Backends

JadeTE gehört begrifflich zur Jade-Plattform, aber nicht notwendig in die laufende JadeRF, da die bisherige Architektur Typsemantik materialisiert und zur normalen Laufzeit keine TypeEngine benötigt.

Die zugrunde liegende Trennung bleibt:

JDL besitzt Semantik. JadeRF besitzt privilegierte Mechanik.

I.4 Speicherbesitz als Architektur- und Observability-Grenze

Eine weiterführende Überlegung ist, Speicherbesitz nicht global und undifferenziert zu behandeln, sondern an bestehende Architekturgrenzen zu koppeln.

Für die CompilerDB bietet sich an:

Jeder Store besitzt seine Daten und bestimmt seine Speicherpolitik.

Beispiel:

CompilerDB
├── SeedStore
│   └── SeedRegion
├── DescriptorStore
│   └── DescriptorRegion
├── Cache
│   └── CacheRegion
├── DepGraph
│   └── GraphRegion
└── ProvenanceStore
    └── ProvenanceRegion

Die Stores haben ohnehin unterschiedliche Lebensdauer- und Mutationsprofile:

SeedStore          praktisch unveränderlich innerhalb des Laufs
DescriptorStore    append-only und interned
Cache              ersetzbar und verwerfbar
DepGraph           mutable Graphstruktur
ProvenanceStore    revisionsgebunden und potenziell kompaktierbar

Es wäre daher unnötig, ihnen allen dieselbe konkrete Allokationsstrategie aufzuzwingen.

Für JadeRF sollte die Grenze etwas anders heißen:

Jedes zustandsbehaftete Subsystem besitzt semantisch seine Speicherdomäne; ein Port besitzt keinen Zustand und keinen Speicher.

Also nicht:

SchedulerPort besitzt Scheduler-Speicher
JadeME-Port besitzt JadeME-Speicher
FfiPort besitzt FFI-Speicher

sondern:

Scheduler
    └── SchedulerRegion
        ↑
    SchedulerPort

JadeME
    └── JadeME-interne Regionen
        ↑
    JadeME-Port

Der Port ist die Zugriffsschnittstelle, nicht der Besitzer.

Keine rohen Speicherreferenzen über Grenzen

Eine starke Folgeregel wäre:

Store- und Subsystemgrenzen werden nicht durch rohe interne Speicherreferenzen überschritten.

Stattdessen werden stabile oder kontrollierte Identitäten verwendet:

CompilerDB   → typed IDs
JadeRF       → Handles / Commands
Artefakte    → relocatable references

Damit kann ein Subsystem seine interne Speicherrepräsentation ändern, ohne dass fremde Subsysteme an seinen Zeigern hängen.

I.5 JadeME als zentrale mechanische Speicherinstanz

Die vorige Regel bedeutet nicht, dass jedes JadeRF-Subsystem seinen eigenen vollständigen Allocator-Stack unabhängig von JadeME bauen sollte. Da JadeME bereits die Memory Engine ist, bietet sich ein stärker integriertes Modell an:

JadeME
    besitzt übergeordneten Runtime-Speicher
        ↓
Subsystem fordert Speicherdomäne an
        ↓
JadeME erzeugt MemoryRegion
        ↓
Subsystem erhält kontrollierte Region-/Allocator-Sicht
        ↓
Subsystem arbeitet innerhalb dieser Region

Damit besitzt das Subsystem seine Domäne semantisch, während JadeME die physische Speichermechanik und deren Lebenszyklus kontrolliert.

Eine passende Verantwortlichkeit wäre:

Subsystem
    entscheidet:
    Welche Art Speicher brauche ich?

JadeME
    entscheidet:
    Wie stelle ich diesen Speicher sicher bereit?

Accounting
    beantwortet:
    Wie viel davon wird verbraucht?

Control Path gegen Data Path

Nicht jede einzelne Allocation sollte als RuntimeBus-Command durch JadeME laufen. Das wäre für Hot Paths unnötiger Dispatch.

Sinnvoller ist eine Zweiteilung:

Control Path
────────────
RuntimeBus oder privilegierte interne API
→ create region
→ resize budget
→ reset region
→ destroy region
→ query statistics

Data Path
─────────
AllocatorView / RegionView
→ alloc
→ free oder reset, abhängig von der Policy

Die Region wird einmal autorisiert und aufgelöst; danach kann ein privilegiertes Subsystem lokal gegen seine kontrollierte Sicht arbeiten.

RuntimeBus eignet sich für Lebenszyklus und Autorität; der Allocation-Hot-Path braucht direkte privilegierte Mechanik.

I.6 MemoryRegion als allgemeine Abstraktion

Eine ScratchArena passt in dieses Modell, sollte aber nicht die oberste Abstraktion sein.

Die allgemeinere Einheit wäre eine MemoryRegion oder MemoryDomain mit Eigenschaften wie:

owner
lifetime
policy
budget
accounting
threading / sharing policy

Mögliche Policies:

MemoryRegion
├── ScratchArena
├── PersistentArena
├── RevisionArena
├── Pool
├── Slab
└── weitere spezialisierte Policies

Eine ScratchArena wäre dann eine kurzlebige Region mit Bulk-Reset:

allocate
allocate
allocate
allocate
reset

Typische Kandidaten wären temporäre Parser-/Codegen-Daten, Serialisierungspuffer oder FFI-Marshalling. Eine andere Region kann dagegen einen Pool oder eine persistente Arena benötigen.

Wichtig ist die Trennung:

MemoryRegion   = Ownership-, Lifetime-, Budget- und Accounting-Domäne
Policy         = konkrete Allokationsstrategie innerhalb dieser Domäne

I.7 Zig als mechanische Basis der JadeME

Dieses Modell steht nicht im Widerspruch zu Zig. Im Gegenteil: Es kann Zigs explizites Allocator-Modell als mechanische Basis benutzen.

Konzeptionell:

OS / Host Memory
      ↓
JadeME Parent Allocator
      ↓
MemoryRegion
      ↓
regionspezifischer Allocator
      ↓
JadeRF-Subsystem

JadeME würde Zigs Allocator-Modell also nicht ersetzen, sondern organisieren und um Jade-spezifische Regeln ergänzen:

Zig Allocator
    mechanische Allokationsschnittstelle

JadeME MemoryRegion
    Ownership
    Lifetime
    Policy
    Budget
    Accounting
    kontrollierte Identität

Ein möglicher JadeME-Wrapper könnte vor der konkreten Allokationspolicy Accounting und Budgetprüfung durchführen:

JadeRegionAllocator
    ↓
Accounting
    ↓
Budget check
    ↓
konkrete Policy
    ↓
Parent Allocator

Für das Monitoring sollten mindestens unterschiedliche Größen getrennt werden, weil insbesondere Arenen Speicher zurücksetzen können, ohne ihn sofort an den Host zurückzugeben:

reserved
committed / allocated
used
peak
allocation count

Auch Threading gehört zur Region-Policy. Denkbare Kategorien wären etwa:

ThreadLocalScratch
SingleOwner
SharedSynchronized
SharedLockFree

Diese Namen sind illustrative Kategorien, keine bereits festgelegten Jade-Typen.

I.8 JadeME intern genauso benutzen wie von JDL aus

Ein besonders attraktiver Effekt wäre, dass JadeRF ihre eigene Memory Engine nicht umgeht.

JDL-Welt
→ jade::memory API
→ RuntimeBus / materialisierte Mechanik
→ JadeME

JadeRF-intern
→ privilegierte JadeME-API
→ MemoryRegion
→ AllocatorView

Die Zugriffsebene unterscheidet sich, die zugrunde liegenden Mechanismen bleiben dieselben:

Region
Ownership
Lifetime
Policy
Budget
Accounting
Handle / Identity

Damit gäbe es nicht zwei Speicherarchitekturen wie „JDL-Arena“ und daneben eine völlig andere interne Zig-Arena. JadeRF würde dieselben primitiven Mechanismen selbst benutzen, die sie JDL später in kontrollierter Form zugänglich macht.

Das ist kein vollständiges Self-Hosting, aber eine wichtige Form von semantischer Kontinuität zwischen privilegierter Runtime und JDL-Oberfläche.

I.9 Zentrale Ressourcenbuchhaltung bei lokaler Ownership

Die getrennten Speicherregionen liefern zugleich natürliche Observability-Grenzen.

Die gewünschte Struktur ist:

lokale Ownership
+ lokale Speicherpolitik
+ zentrale Ressourcenbuchhaltung

Nicht ein zentraler Allocator soll jede lokale Strategie bestimmen. Zentralisiert wird nur die Sicht auf den Verbrauch.

Für die CompilerDB könnten etwa sichtbar werden:

CompilerDB Memory
├── SeedStore
│   ├── reserved
│   ├── used
│   └── peak
├── DescriptorStore
│   ├── descriptor count
│   ├── payload bytes
│   └── interning index
├── Cache
│   ├── live entries
│   ├── overflow
│   └── hit / miss
├── DepGraph
│   ├── nodes
│   └── edges
└── ProvenanceStore
    ├── records
    ├── retained revisions
    └── reclaimed bytes

Für JadeRF entsprechend:

JadeRF Resources
├── JadeME
│   ├── regions
│   ├── reserved
│   ├── used
│   └── peak
├── Scheduler
│   ├── tasks
│   ├── runnable
│   ├── suspended
│   └── queue memory
├── Messaging
│   ├── mailboxes
│   └── queued bytes
├── FFI
│   ├── foreign handles
│   └── temporary buffers
└── Backend Runtime
    ├── JBC
    ├── WASM
    └── weitere Backends

Damit könnte Mission Control später nicht nur melden, wie viel Speicher ein Prozess benutzt, sondern welches Subsystem ihn besitzt und aus welchem Grund.

Die drei Begriffe sollten dabei strikt getrennt bleiben:

Ownership
    Wer besitzt den Speicher?

Policy
    Wie verwaltet der Besitzer ihn?

Accounting
    Wie viel davon wird verbraucht?

Von Monitoring zu Resource Governance

Wenn Verbrauch bereits pro Domäne erfasst wird, können dieselben Grenzen später Budgets tragen:

ProvenanceStore <= Budget
Compiler Cache  <= Budget
PluginContext   <= Budget
RequestContext  <= Budget

Dabei könnten unterschiedliche Reaktionen möglich sein:

soft budget
→ Telemetrie / Warnung / Eviction oder Kompaktion

hard budget
→ kontrollierte Ablehnung weiterer Ressourcen

Welche Budgets tatsächlich Teil eines ContextDescriptor oder einer anderen Policy werden, ist offen. Die Architekturgrenze macht diese spätere Governance jedoch möglich, ohne ein zweites Resource-System einzuführen.

Speicher wird lokal besessen und verwaltet, aber global bilanziert und beobachtet.

I.10 Logische Transaktionen trotz physisch getrennter Regionen

Getrennte Speicherbereiche verhindern keine gemeinsamen atomaren Operationen.

Bei einer CompilerDB-Auswertung können mehrere Stores logisch gemeinsam betroffen sein:

Cache
DepGraph
DescriptorStore
ProvenanceStore

Sie müssen deshalb nicht denselben Allocator oder dieselbe Region teilen. Stattdessen kann die Operation Daten zunächst in den jeweiligen Besitzdomänen vorbereiten und erst danach gemeinsam publizieren:

1. Daten in privaten Zielregionen vorbereiten
2. Validierung abschließen
3. atomarer Publish Point
4. neue Roots / IDs / Ergebnisse sichtbar machen

Damit bleiben zwei Dinge getrennt:

physische Ownership   getrennt
logische Transaktion  gemeinsam

Dasselbe Prinzip ist auf JadeRF übertragbar: Scheduler, JadeME und Messaging können an derselben Laufzeitoperation beteiligt sein, ohne gegenseitig ihren internen Speicher zu besitzen.

I.11 Intrinsics, RuntimeBus-Commands und Syscalls

Ein Intrinsic sollte nicht als bloßer Syscall-Wrapper verstanden werden.

Die präzisere Rolle lautet:

Ein Intrinsic ist eine von der VM bekannte primitive Operation, deren Semantik JDL zugänglich ist, deren privilegierte Mechanik aber JadeRF bereitstellt.

Ein Intrinsic kann ohne Syscall auskommen:

handle_validate
arena_reset
task_yield
atomic_compare_exchange
trap

Ein anderes Intrinsic kann mittelbar einen oder mehrere Host-Syscalls benötigen:

file_read
socket_send
memory_region_create
poll_wait

Die Ebenen bleiben verschieden:

normale JDL-Funktion
    vollständig in JDL ausdrückbar

Intrinsic
    VM-nahe primitive Jade-Operation

RuntimeBus Command
    reihenfolgenabhängige Operation an einem JadeRF-Subsystem

Syscall
    Übergang von JadeRF zum Linux-Kernel

Daher gilt ausdrücklich:

Intrinsic ≠ Syscall
Intrinsic ≠ RuntimeBus Command
RuntimeBus Command ≠ Syscall

Sie können aber aufeinander abgebildet werden:

JDL call
→ Intrinsic
→ optional RuntimeBus Command
→ JadeRF-Subsystem
→ optional Linux syscall

Nicht jeder Syscall braucht ein Intrinsic

Ein JadeRF-Subsystem kann Linux-Syscalls intern verwenden, ohne diese als JDL-/VM-Primitiv sichtbar zu machen.

Beispiele:

JadeME
→ mmap / munmap / mprotect

Scheduler
→ futex / epoll / eventfd / weitere Host-Mechanik

Daraus folgt keine Notwendigkeit für IntrinsicMmap, IntrinsicEpollWait oder ähnliche 1:1-Abbildungen.

Die Kardinalität ist frei:

1 Intrinsic → 0 Syscalls
1 Intrinsic → 1 Syscall
1 Intrinsic → mehrere Syscalls
mehrere Intrinsics → derselbe Syscall

Die JadeRF sollte deshalb keine typisierte Kopie der Linux-Syscall-Tabelle werden. Intrinsics drücken Jade-Primitiven aus; Syscalls bleiben Implementierungsmechanik darunter.

I.12 Zig, libc und direkter Linux-Zugriff

Für die Linux-only-Ausrichtung von Jade ist attraktiv, dass die Trusted-Mechanik nicht grundsätzlich libc als Zwischenlaufzeit benötigt.

Der besprochene Zielgedanke ist:

JadeRF
→ Zig
→ Linux-spezifische Low-Level-Schicht
→ Linux Kernel ABI

Libc bleibt dort sinnvoll, wo sie bewusst Teil der gewählten ABI-Grenze ist, insbesondere beim C-FFI:

JDL
→ Jade Bridge / FFI
→ C ABI
→ libc und/oder externe .so

Dagegen können Kernmechanismen wie JadeME oder Scheduler möglichst direkt auf Linux-Mechanik aufbauen.

Die konkreten Zig-Stdlib-APIs sind Implementierungsdetail und versionsabhängig. Die architektonische Aussage ist deshalb nicht „Jade hängt an einer bestimmten Zig-Funktion“, sondern:

Zig soll den architekturspezifischen Zugriff auf das Linux-ABI mechanisch kapseln; JadeRF soll daraus eigene Jade-Primitiven bauen.

I.13 Eine minimale Jade Host Layer

Aus den Überlegungen zu Syscalls ergibt sich eine weitere mögliche Grenze: JadeRF sollte Linux nicht beliebig und von jedem Subsystem aus ansprechen, sondern eine kleine, kontrollierte Host-Anbindung besitzen.

Konzeptionell:

Linux Kernel ABI
        ↓
Jade Host Layer
        ↓
JadeRF
        ↓
JadeVM / JadeME / Scheduler / FFI / Backends

Jade Host Layer ist hier ein Arbeitsbegriff, kein beschlossener neuer öffentlicher Systemname. Gemeint ist die minimale kontrollierte Menge an Linux-Mechanik, die JadeRF benutzen darf.

Die zentrale Frage lautet damit nicht mehr:

Welchen Linux-Syscall soll Jade benutzen?

sondern:

Welche minimale Linux-Maschine soll JadeRF überhaupt sehen?

Diese Grenze verhindert, dass Linux-spezifische Begriffe unkontrolliert in höhere Jade-Schichten durchsickern.

I.14 Untersuchungsfelder des Host-Vertrags

Für diesen Host-Vertrag wurden folgende Bereiche als besonders wichtig identifiziert. Die Liste ist eine Untersuchungsagenda, keine Entscheidung für bestimmte Syscalls oder APIs.

Virtueller Speicher

Zu klären sind unter anderem:

virtuelle Reservierung
Commit / Decommit beziehungsweise tatsächliche physische Nutzung
Guard Pages
Speicherschutz
W^X für ausführbaren Code
große Regionen

Dieser Bereich berührt direkt JadeME und spätere native beziehungsweise WASM-basierte Backends.

Threads und Memory Model

Zu klären sind:

Host-Threads
Futex-artige Primitive
Atomics
Memory Ordering
Sichtbarkeit zwischen Worker-Threads
Cache-Kohärenz als angenommene Host-Eigenschaft

Das ist besonders kritisch, weil ein unpräzises Speichermodell nicht nur Performance, sondern Korrektheit von lock-freiem und geteiltem Zustand betrifft.

I/O und Event-Mechanik

Zu untersuchen sind unter anderem:

nonblocking file descriptors
epoll-artige Event-Schleifen
io_uring-artige asynchrone Mechanik
Cancellation
Wakeups
Backpressure

Der Scheduler sollte daraus Jade-eigene Event- und Suspendierungsmechanik bauen, statt Linux-Begriffe nach oben zu propagieren.

Zeit

Es braucht eine klare Trennung verschiedener Zeitbegriffe:

monotonic time
wall / realtime
timer
deadline
sleep

Subsysteme dürfen nicht stillschweigend unterschiedliche Uhren als dasselbe Konzept behandeln.

Signale, Faults und Prozessfehler

Zu klären ist, welche Host-Fehler JadeRF abfängt oder transformiert und welche den Prozess beenden dürfen.

Mögliche Abbildung:

Host fault / signal
→ JadeRF fault handling
→ Trap, kontrollierter Fehlerpfad oder Prozessabbruch

Ein FFI-Crash ist dabei nicht automatisch dasselbe wie ein semantisches Throw[E].

Thread-Lokalität und CPU-Zuordnung

Relevant sind:

thread-local runtime state
worker-local scratch regions
thread identity
CPU affinity
NUMA-/CPU-Topologie, falls später relevant

File Descriptors und Host-Ressourcen

Ein Linux-FD ist eine lauf-lokale, wiederverwendbare Host-Identität. Deshalb sollte geklärt werden:

wer besitzt ihn?
wer darf ihn schließen?
wie wird Ownership übertragen?
wie werden stale / reused FDs verhindert?

Das Jade-Handle-Modell könnte darüber eine sicherere HostHandle-Abstraktion legen.

Loader und Dynamic Linking

Für FFI, Plugins und native Artefakte sind zu untersuchen:

ELF
.so loading
Symbolauflösung
Relocations
ABI-Grenzen
Lifecycle geladener Bibliotheken

Ausführbarer Code

JBC, WASM-native Code und spätere Native-Backends brauchen klare Regeln für:

Code allocation
Write-Phase
Execute-Phase
W^X
Freigabe
Invalidierung

Dauerhaft RWX-Speicher sollte nicht die implizite Standardlösung werden.

CPU-Features

Backend-Auswahl kann Host-Fähigkeiten benötigen:

SIMD
AVX / AVX2 / AVX-512 oder andere x86-64-Level
spätere weitere Architekturen

Diese Fähigkeiten sollten als Host-Beschreibung materialisiert werden, nicht ad hoc in beliebigen Backends abgefragt werden.

Entropie und Zufall

Kernel-Entropie ist eine Host-Ressource. Zu unterscheiden ist zwischen:

kryptografischer Entropie
PRNG-Zustand
reproduzierbarem deterministischem Zufall

Nicht jede Zufallsanforderung sollte automatisch einen Kernelzugriff bedeuten.

Crash-Diagnostik

Mögliche Host-Daten für Diagnose:

fault address
signal / trap reason
native stack context
Jade frame context
core-dump integration
symbolization

Die Ausführung darf davon nicht abhängen; für Mission Control und Debugging kann es dennoch zentral sein.

Resource Limits und Host-Isolation

Langfristig können Jade-Budgets mit Host-Mechanismen gekoppelt werden:

rlimits
cgroups
namespaces
weitere Linux-Isolationsmechanismen

Das wäre eine mechanische Verstärkung von Jade-eigener Resource Governance, nicht deren Ersatz.

Sandboxing und Syscall-Reduktion

Linux-Mechanismen wie seccomp sind konzeptionell interessant, weil Jade die aus Context-/Resource-Governance resultierende Host-Autorität nicht nur logisch, sondern teilweise auch auf Host-Ebene einschränken könnte.

Die genaue Abbildung ist offen. Der interessante Gedanke lautet:

Jade Resource-/Context-Policy
        ↓
materialisierte Host-Autorität
        ↓
optional mechanisch eingeschränkter Syscall-Raum

Damit würde das Capability-Modell an besonders harten Grenzen nicht nur auf korrekter JadeRF-Implementierung beruhen.

I.15 Host-Mechanik sofort in Jade-Begriffe übersetzen

Die Host Layer sollte Linux-Begriffe möglichst früh in Jade-eigene Begriffe überführen.

Illustrative Zuordnung:

mmap / virtuelle Host-Region   → MemoryRegion
futex-artige Primitive        → Scheduler primitive
epoll / io_uring              → Event source / I/O mechanism
file descriptor               → HostHandle
signal / protection fault     → Trap oder definierter Fault-Pfad
.so symbol                    → FFI binding

Diese Zuordnungen sind keine Aussage, dass jede linke Operation exakt einer rechten entspricht. Sie drücken die gewünschte Richtung aus:

Linux liefert Mechanik; JadeRF benennt und begrenzt sie in Jade-Semantik.

I.16 Nursery und Structured Concurrency

Nursery ist hier als nicht-normative Architekturidee für die strukturierte Lebensdauer von Tasks zu verstehen. Der Begriff gehört semantisch zum Task- und Scheduler-Modell, nicht zur CompilerDB und nicht zur JadeME.

Eine Nursery ist eine Laufzeitdomäne für strukturierte Task-Ownership. Jeder ausführbare Context besitzt eine Root-Nursery; Tasks können darin weitere Nurseries öffnen. Jeder Task gehört während seiner gesamten Lebensdauer genau einer solchen Ownership-Nursery.

Sie beantwortet nicht, wann ein Task CPU-Zeit bekommt — das bleibt Scheduler-Mechanik. Sie beantwortet, welche laufenden Berechnungen zusammengehören, wer sie besitzt und wann ihre gemeinsame Lebensdauer endet.

Wer eine Nursery besitzen kann

Eine Nursery ist keine alternative Form eines Contexts und keine zusätzliche Ausführungsdomäne. Sie ist die allgemeine Ownership-Domäne für Tasks — und ihr Eigentümer muss kein Task sein:

Owner einer Nursery
├── Context                     die Root-Nursery
├── Task                        eine verschachtelte Nursery
└── langlebige Runtime-Entität  etwa eine EngineInstance

Daraus folgt die Verschachtelung:

Context
└── Root-Nursery                entsteht mit dem Context, darf leer sein
    └── Root Task
        └── Nested Nursery
            ├── Child A
            └── Child B

Der Context bringt seine Root-Nursery mit. Sie entsteht mit ihm und endet mit ihm, auch wenn nie ein Task darin läuft. Damit hat der erste Task einen Besitzer, ohne dass ihn jemand benennt — und die Regel „eine Nursery überlebt ihren Context nicht" wird zur Definition statt zur Zusage.

main fällt deshalb nicht auf: Es läuft in der Root-Nursery des Root-Contexts.

Ein RequestContext sieht einer Nursery nur ähnlich, weil beide viele kurzlebige Instanzen haben. Er ist trotzdem eine Ebene darüber — und bringt seine eigene Root-Nursery mit.

Woher ein Task seine Nursery bekommt

Nicht jeder Task entsteht durch spawn. Der Wurzel-Task tut es nicht, ein Actor-Task nicht, ein Kombinator ruft es nicht selbst auf, und ein FFI-Callback wird von fremdem Code ausgelöst.

Jeder Task entsteht in genau einer Ownership-Nursery. Ein Spawn bindet an die aktuell aktive verschachtelte Nursery. Existiert keine, bindet er an die Nursery, die den erzeugenden Task selbst besitzt. Nur wenn kein erzeugender Task existiert, gilt die Root-Nursery des Contexts. Ist auch kein Context herleitbar, muss die Eintrittsbindung den Context und die Regel zur Bestimmung der Ownership festlegen.

Die Auflösung läuft also in vier Stufen:

1  aktive verschachtelte Nursery
       ↓ sonst
2  Nursery, die den erzeugenden Task besitzt
       ↓ kein Task vorhanden
3  Root-Nursery des Contexts
       ↓ kein Context herleitbar
4  Eintrittsbindung nennt Context und Ownership-Regel

Stufe 2 ist die wichtigste, und sie fehlt leicht. Ein Task hat immer eine Nursery, die ihn besitzt, auch wenn er selbst keine geöffnet hat:

Context
└── Root-Nursery
    └── Task A
        └── Nursery X
            └── Task B
                └── spawn C

C gehört zu Nursery X — derselben, die B besitzt. Fiele die Auflösung hier auf die Root-Nursery zurück, wäre C aus der Lebensdauer von X entkommen, und der Fallback wäre ausgerechnet der Mechanismus, mit dem sich structured concurrency umgehen lässt.

Aktiv, nicht irgendeine. Bei Stufe 1 gilt dasselbe eine Ebene feiner. Ein Task kann mehrere verschachtelte Nurseries offen haben:

Task
└── Nursery A
    └── Code
        └── Nursery B   ← aktiv
            └── spawn X

X gehört zu B, nicht zu A.

Damit sind die vier Fälle gedeckt:

Wurzel-Task       Root-Nursery des Contexts, keine Herleitung nötig
Spawn             aktive Nursery des erzeugenden Tasks
Kombinator        öffnet eine Nested Nursery und startet darin
Fremdeintritt     die Bindung nennt Context und Ownership-Regel

Beim Fremdeintritt ist die entscheidende Achse nicht synchron gegen aufgeschoben, sondern ob ein Jade-Task herleitbar ist:

reentrant          Jade Task → FFI-Aufruf → fremde Funktion → Callback → derselbe Task
                   kein neuer Task, also keine neue Ownership

foreign-entry      fremder Thread → Callback → kein Jade-Task darunter
                   Bindung nennt Context und Ownership-Regel

Beim reentranten Fall entsteht kein neuer Task — der Callback läuft im bestehenden. Es gibt also nichts zu binden. Erzeugt er selbst Tasks, gilt dieselbe vierstufige Auflösung wie für den umgebenden Task.

Beim Fremdeintritt enthält die Bindung keine Nursery. Eine CallbackBinding ist eine geprüfte Beschreibung; eine Nursery ist lebender Zustand. Was dort steht, ist die Regel, nach der zur Laufzeit aufgelöst wird:

CallbackBinding
├── Ziel-Context
├── Ownership-Regel
└── weitere Eintrittsregeln
        ↓  zur Laufzeit aufgelöst
autorisierte Nursery
        ↓
neuer Jade-Task

Nicht ein Handle auf ein lebendes Objekt. Das ist dieselbe Trennung, aus der auch ParentExpose als Weg für eine Nursery ausgeschlossen ist — und sie lässt offen, welche Form die Autorität später bekommt (§H.2).

Eine C-Bibliothek darf denselben Callback aus ihrem eigenen Threadpool aufrufen. Der ist dann synchron aus ihrer Sicht und hat trotzdem keinen Jade-Aufrufer. Wer nur nach sync und deferred unterscheidet, übersieht genau diesen Fall.

Wer zuerst endet

Eine Nursery darf die Ausführungsdomäne, in der sie existiert, nicht überleben.

Ein Kind hält möglicherweise eine Service-Bindung aus seinem Context. Endet der Context, ist die Bindung weg. Also müssen die laufenden Task-Strukturen in einen terminalen Zustand gebracht werden, bevor der Context seine Bindungen abbaut.

Wie genau — geordnetes Drain, Deadline, anschließende Cancellation oder eine Kombination — ist Scheduler- und Context-Policy, keine Eigenschaft des Nursery-Begriffs. Eine Anfrage mit Zeitlimit, ein Plugin-Unload und ein Anwendungs-Shutdown haben verschiedene Fristen (§H.2).

Eine Nursery ist keine Contextgrenze

Child Tasks einer Nursery laufen immer in derselben Ausführungsdomäne wie diese Nursery. Wird Arbeit in einem anderen Context ausgeführt, entsteht dort ein eigener Task unter dessen Nursery-Struktur — und dieser Task ist kein Child der ursprünglichen Nursery.

Die Nursery selbst installiert keine Services, verleiht keine Resource-Autorität, verändert keine Bindings und erzeugt keine Isolation. Sie strukturiert ausschließlich Ownership, Lebensdauer, Join, Failure-Propagation und Cancellation.

Wer Isolation für parallel gestartete Arbeit benötigt, führt diese Arbeit in einem anderen Context aus. Ab diesem Übergang kann der dort laufende Task nicht mehr Child derselben Nursery sein. Die lokale Nursery besitzt lediglich die Bridge-Operation; der Ziel-Context besitzt seine eigene Task- und Nursery-Struktur. Isolation ist damit eine Context-Beziehung und keine Eigenschaft der Nursery. Welche Serviceinstanzen und Speicherbereiche mehrere Tasks tatsächlich teilen dürfen, bleibt Sache der Binding-, Share- und Storage-Policy.

Keine Nursery über Contextgrenzen

Eine Nursery kann ausschließlich Tasks derselben Ausführungsdomäne besitzen, in der sie selbst lebt.

Ein Task in einem anderen Context kann niemals Child derselben Nursery sein. Das ist keine Konvention, sondern folgt aus der Bridge-Regel: Zwischen isolierten Domänen führt nur der deklarierte Pfad, und eine Nursery, die darüber hinweggriffe, wäre ein zweiter Weg daran vorbei.

Local Nursery
    └── lokale Bridge-Operation
            ↓ Bridge
        anderer Context
            └── eigene Task- und Nursery-Struktur

Was die lokale Nursery besitzt, ist die lokale Bridge-Operation — nicht der fremde Task.

Das ist auch praktisch richtig: Ein Plugin-Task, den der Host per Nursery abwürgen kann, wäre keine Isolation.

Cancellation über eine Bridge

Ob das Ende einer Bridge-Operation drüben etwas bewirkt, darf kein implizites Transportdetail sein. Es gehört in den materialisierten Vertrag:

BridgeDescriptor.cancellation
├── unsupported
└── propagate-request

propagate-request heißt: Das Ende oder die Cancellation der lokalen Operation darf eine Cancellation-Anforderung an die Gegenseite senden. Es heißt ausdrücklich nicht, dass die lokale Nursery den fremden Task besitzt oder seine Terminierung garantieren kann. Ob und wie die Gegenseite reagiert, entscheidet ihre eigene Domäne.

unsupported heißt nicht, dass die lokale Operation hängen bleibt. Sie darf enden; ein weiterlaufender fremder Task bleibt Eigentum seiner Domäne, und ein spätes Ergebnis wird verworfen.

Ohne diesen Eintrag hätte der Aufrufer eine Erwartung, die manchmal zutrifft — und das ist schlimmer als eine, die nie zutrifft.

Eine Nursery besitzt Tasks, keine Ressourcen

Der naheliegende Fehlschluss: Ein Task ruft instantiate auf, also gehört die entstandene EngineInstance seiner Nursery.

Das stimmt nicht, und die Folge wäre unangenehm:

Request Task
    → erzeugt einen Actor
    → Request-Nursery endet
    → Actor müsste sterben

Obwohl der Actor vielleicht anwendungsweit leben soll. Das ist derselbe Lifetime-Handoff wie bei der Hintergrundarbeit, nur an anderer Stelle.

Die Trennung lautet deshalb:

Nursery                  besitzt Tasks
Resource-Ownership       besitzt EngineInstances und andere Runtime-Ressourcen
EngineInstance           besitzt wiederum die Nursery ihrer internen Tasks

Das ist sauber rekursiv, ohne aus der Nursery den Universalbesitzer zu machen:

Context
├── Root-Nursery              Task-Ownership
└── Runtime-Ressourcen        Resource-Ownership
    └── EngineInstance
        └── interne Nursery   Task-Ownership

Wer eine EngineInstance besitzt — ein Context, ein Resource-Scope, eine Plugin-Lebensdauer oder eine ausdrücklich übertragene Ownership —, bestimmt, wann sie endet. Endet sie, schließt ihre Nursery, und ihre Tasks werden terminal.

Damit ist nebenbei beantwortet, was das Ende einer EngineInstance für ihre laufenden Tasks bedeutet.

Arbeit mit längerer Lebensdauer

Structured Concurrency verbietet, dass ein Child seine Ownership-Domäne unbemerkt überlebt. Bei kurzlebigen Contexts wird das schnell konkret: Ein Handler will ein Log schreiben, einen Cache wärmen, eine Nachricht zustellen — Arbeit, die die Anfrage überdauern soll.

Nach der Regel geht das nicht, und das ist richtig so. Genau diese Sorte Arbeit verschwindet sonst in Systemen, und niemand weiß, wer sie besitzt.

Der Ausweg ist eine ausdrückliche Übergabe:

Request Task
    ↓ explizite Übergabe
Application Task Domain
    └── Background Task

Eine Task kann ihre eigene strukturierte Lebensdauer nur verlassen, indem eine andere, länger lebende Ownership-Domäne die Verantwortung ausdrücklich übernimmt.

Ein natürliches Ziel ist die Root-Nursery einer länger lebenden Ausführungsdomäne — etwa die des ApplicationContext. Sie existiert ohnehin und braucht keinen neuen Mechanismus.

Sie ist aber nicht das einzige. Ebenso kann eine andere langlebige Runtime-Entität die Ownership übernehmen, sofern sie eine eigene Nursery besitzt und der Aufrufer dafür autorisiert ist:

ApplicationContext → Root-Nursery      der allgemeine Fall
EngineInstance     → interne Nursery   ebenso möglich

Erreichbar sind sie damit nicht.

Eine Nursery existiert als Ownership-Ziel; Erreichbarkeit ist eine separate Autoritätsfrage.

Wer eine adressieren will, braucht eine gewährte Fähigkeit — konzeptionell etwa ein TaskSpawner für eine bestimmte Lebensdauer. Nicht einen Zugriff der Art ApplicationContext.rootNursery; das wäre der Weg an der eigenen Lebensdauer vorbei, und structured concurrency wäre wieder eine Empfehlung.

Die Form ist offen. Festgelegt ist, was sie nicht ist: Eine Nursery wird nicht über ParentExpose sichtbar gemacht. Parent-Expose gehört zur statischen Context-Verdrahtung, eine Nursery ist laufender Zustand — ein laufendes Objekt hat in einer Deskriptorliste nichts zu suchen (§H.2).

Damit bleiben drei Dinge getrennt, die man gern zusammenwirft: Existenz, Ownership und Autorität.

Nursery-Instanz gegen Policy — und wohin Policy gehört

Eine laufende Nursery ist kein Descriptor. Sie hält dynamischen Zustand: Parent, Kinder, aktive Zahl, Completion-, Cancellation- und Failure-Zustand.

Damit gilt auch hier das Muster aus F.2 — links die geprüfte Beschreibung, rechts das Lebende. Nur zerfällt die linke Seite in mehrere Ebenen, und die zusammenzufassen wäre der bequeme Fehler:

feste Nursery-Semantik        Ownership · Scope-Ende · Join und Drain
                              keine entweichenden Child Tasks
                              → gehört zur Sprache, nicht zur Konfiguration

Kombinator-Semantik           race · all · firstSuccess · timeout
                              deren Failure- und Cancellation-Regeln
                              → folgt aus der verwendeten JDL-Abstraktion

ContextDescriptor.limits      Task- und Child-Budgets, Resource Limits

ContextDescriptor.scheduler   Scheduler- und Shutdown-Mechanik,
                              Deadline-Verhalten

Der Prüfstein: race(A, B) muss unabhängig vom Context dieselbe Bedeutung haben. Der Context darf festlegen, ob überhaupt genügend Tasks und Ressourcen verfügbar sind — er darf nicht die Fehlersemantik von race verändern.

Deshalb ist failure propagation keine frei konfigurierbare Context-Policy. Ob eine Abstraktion bei einem Fehler Geschwister cancelt, ist beobachtbare Programmlogik. Sie darf nicht davon abhängen, in welchem Deployment ein Programm gerade läuft.

max children ist dagegen ein Ressourcenlimit und passt zu limits. Eine Shutdown-Deadline betrifft das Beenden der Ausführungsdomäne und gehört zur Scheduler-Konfiguration.

Kein NurseryDescriptor — jedenfalls jetzt nicht. Nach dem Kriterium aus C.1 braucht ein Deskriptor eine tiefere Schicht, die ihn konsumiert. Solange die Limits im ContextDescriptor stehen und der Scheduler sie von dort liest, gibt es keinen Konsumenten für einen eigenen. Ihn vorsorglich anzulegen wäre genau die Inflation, die schon sechsundsechzig Deskriptornamen erzeugt hat.

Die Schichtung im Ganzen

JDL Structured Concurrency   definiert Bedeutung
    Nursery · race · all · timeout

ContextDescriptor            begrenzt Ausführung
    limits · scheduler configuration

Scheduler                    implementiert Mechanik

BridgeDescriptor             beschreibt Cross-Context-Eigenschaften
    einschließlich Cancellation-Propagation

Task-Outcomes

Die Nursery erfindet keine neuen Endzustände. Sie strukturiert die vorhandenen:

Success(value)
Failure(error)
Trap(info)
Cancelled

Eine mögliche Policy einer konkreten Abstraktion:

B schlägt fehl
    ↓
Nursery wechselt in einen failing state
    ↓
C wird kontrolliert cancelled
    ↓
alle Children werden gejoint und gedrained
    ↓
Failure wird am Nursery-Rand an den Parent weitergegeben

Der Scheduler führt Cancellation und Join mechanisch aus; welche Failure-Regel gilt, bestimmt die verwendete Abstraktion.

Daraus ableitbare Kombinatoren

Auf Task, Nursery, Join und Cancellation lassen sich höhere JDL-Abstraktionen aufbauen:

race · all · firstSuccess · timeout
bounded parallelism · hedged execution

Keine davon braucht ein eigenes Scheduler-Primitiv. race(A, B) ist semantisch eine gemeinsame Nursery, in der beide Berechnungen starten, der erste terminale Ausgang gewinnt und die verbleibende kontrolliert beendet wird. firstSuccess bleibt davon getrennt: Dort gewinnt ausdrücklich der erste erfolgreiche Ausgang.

Das ist das eigentliche Argument dafür, dass die Nursery fundamental ist. Der Scheduler bleibt klein, während die JDL-Ebene reichere Structured-Concurrency-Abstraktionen ausdrücken kann.

Nursery gegen Supervisor

Nursery      Welche Tasks gehören zusammen?
             Wann dürfen sie enden?
             Wie werden sie gejoint?
             Wie propagieren Failure und Cancellation?

Supervisor   Soll ein fehlgeschlagener Task neu gestartet werden?
             Mit welcher Backoff-Policy?
             Welche Geschwister betrifft der Fehler?
             Wann wird endgültig aufgegeben?

Ein Supervisor ist eine mögliche höhere Recovery-Abstraktion über strukturierter Task-Ownership, kein anderer Name für die Nursery. Die beiden nicht zu vermischen erspart, dass ein fundamentaler Mechanismus Restart-Strategien mitschleppt.

Verhältnis zum Verzicht auf async/await

Die Nursery ist der strukturelle Gegenpol zum Verzicht auf Funktionsfarbe.

Ohne eine Ownership-Grenze bleibt nach spawn A, spawn B, spawn C offen: Wer besitzt diese Tasks, wann enden sie, wer beobachtet ihre Fehler, wer cancelt sie?

async/await beantwortet diese Fragen nebenbei — um den Preis, dass jede Signatur eingefärbt wird. Die Nursery beantwortet sie ausdrücklich, ohne Suspendierbarkeit in jede Signatur zu propagieren.

Nurseries strukturieren die Lebensdauer konkurrierender Tasks; der Scheduler führt sie aus; die JadeME speichert lediglich ihren Runtime-State; höhere Kombinatoren bauen in JDL darauf auf.

I.17 Zusammengeführte Leitgedanken

Die Überlegungen dieses Teils lassen sich auf einige wiederkehrende Regeln verdichten:

Öffentliche Subsystemnamen
    JadeME / JadeTE / JadeVM / JadeRF / JadeOS

Sprache
    JDL / *.jdl

Namespaces
    kurz und semantisch: jade::memory, jade::vm, jdl::fs, ...

Importstil
    sichtbare Herkunft durch Namespace-Imports, FQN bei Bedarf

CompilerDB
    jeder Store besitzt Daten und Speicherpolitik

JadeRF
    jedes zustandsbehaftete Subsystem besitzt semantisch seine Speicherdomäne

Ports
    vermitteln Zugriff, besitzen aber keinen Subsystemzustand

JadeME
    kontrolliert die physische Runtime-Speichermechanik und liefert Regionen

MemoryRegion
    Ownership + Lifetime + Policy + Budget + Accounting

RuntimeBus
    Control Path für autorisierte Zustandsänderungen, nicht Allocation-Hot-Path

Accounting
    zentral beobachtbar, obwohl Ownership lokal bleibt

Intrinsics
    Jade-Primitiven, keine 1:1-Syscall-Abbildung

Syscalls
    Host-Mechanik unterhalb von JadeRF

Zig
    mechanische Brücke zum Linux-ABI, nicht Quelle der Jade-Semantik

Host Layer
    minimale kontrollierte Linux-Sicht der JadeRF

Nursery
    strukturierte Ownership- und Lebensdauerdomäne für Child Tasks

Scheduler
    führt Join, Cancellation und die von der JDL-Abstraktion verlangte
    Failure-Propagation mechanisch aus

Supervisor
    optionale höhere Recovery-/Restart-Policy über Nurseries

Der übergeordnete Gedanke ist damit derselbe wie an anderen Stellen der Architektur: Eine vorhandene Grenze soll möglichst mehrere Probleme gleichzeitig sauber lösen.

Die Speicherregion kann zugleich Ownership-, Lifetime-, Budget- und Observability-Grenze sein. Der Namespace kann zugleich Herkunft und Gruppierung sichtbar machen. Die Host Layer kann zugleich Portabilitätsgrenze innerhalb des Linux-Ziels, Sicherheitsgrenze und Syscall-Kapselung sein.

Das Ziel ist nicht, möglichst viele Schichten zu erfinden. Das Ziel ist, wenige Grenzen so präzise zu wählen, dass zusätzliche Eigenschaften aus ihnen folgen, statt jeweils ein eigenes Nebensystem zu verlangen.