Wie Jade übersetzt¶
Art: Erklärung, nicht Spezifikation. Zweck: Das Zusammenspiel von CompilerDB, Engines, Meta-Funktionen und Deskriptoren an einem Stück verstehen. Verhältnis zu den Specs: Wo hier etwas anders klingt als in einer normativen Spec, gilt die Spec. Dieses Dokument erklärt, warum die Regeln dort so lauten. Stand: 2026-10-05 — Änderungen gegenüber der Vorfassung am Ende des Dokuments.
1. Der eine Satz¶
Der Compiler leitet nichts ab, was er nicht als Frage gestellt hat — und er schreibt nichts, was nicht geprüft wurde.
Alles Weitere in diesem Dokument ist eine Folge davon. Wenn man diesen Satz im Kopf hat, ergeben sich die meisten Regeln von selbst, statt auswendig gelernt werden zu müssen.
2. Warum das ungewöhnlich ist¶
Ein klassischer Compiler ist eine Pipeline. Etwas geht vorne hinein, durchläuft Phasen, kommt hinten heraus. Jede Phase liest, was die vorige geschrieben hat, und der Zustand liegt in globalen Tabellen, die alle anfassen.
Das funktioniert bei einem einmaligen Durchlauf gut und bei einem Sprachserver gar nicht. Ändert sich eine Zeile, weiß niemand, was davon betroffen ist — also wird alles neu gemacht.
Jade dreht die Richtung um. Es ist keine Pipeline, sondern eine Datenbank mit berechneten Feldern. Nichts läuft, weil es an der Reihe wäre. Etwas läuft, weil jemand danach gefragt hat und die Antwort fehlt.
Klassisch: Pipeline
Parser --> Typprüfung --> Optimierung --> Codegen
Jade: Bedarf
Frage --> Antwort vorhanden?
|
+-- ja ---> zurückgeben <-------+
| |
+-- nein -> Provider läuft |
| |
v |
ablegen ------------+
Der Preis dafür ist Buchhaltung: Man muss wissen, wovon jede Antwort abhängt. Der Gewinn ist, dass eine Änderung nur das ungültig macht, was sie wirklich betrifft.
3. Die CompilerDB¶
3.1 Die Teile und ihre Aufgaben¶
SeedStore (axiomatische Eingaben)
|
v
Provider-Auswertung
|
+--> DepGraph <-----------------------------------------+
| |
+--> direkte Query-Antwort --------+ |
| | |
+--> PatchSet | |
| | | |
| v | |
| Materializer | |
| (prüft und entscheidet) | |
| | | |
| +--> DescriptorStore | |
| | (kanonische Semantik) | |
| | v |
| +------------------> terminaler Ausgang |
| | |
| +--> QueryResult |
| | | |
| | v |
| | Cache <---------------+
| |
v |
Derivation Recorder <---------+
(Query-Mechanik, kein eigener Store)
|
v
ProvenanceStore (Ableitungen und ihre Ausgänge)
Wenn man ein Bild aus der Datenbankwelt will: Der DescriptorStore sind die Tabellen, der Cache ist ein Materialized View, der DepGraph ist die Invalidierungslogik, der Materializer ist die Transaktionsschicht, und der SeedStore sind die Werte, die schon dastanden, bevor jemand etwas eingegeben hat. Der ProvenanceStore ist das Protokoll — nicht der Einträge, sondern der Ableitungen.
Der Unterschied ist wichtiger, als er klingt: Eine Ableitung kann scheitern, und dann gibt es keinen Eintrag, den sie erklären könnte. Erklärt wird, wie eine Frage beantwortet wurde — auch wenn die Antwort lautet, dass es keine gültige gibt (§3.6).
Der Derivation Recorder ist dabei kein eigener Speicher, sondern Query-Mechanik: Er konsumiert den terminalen Ausgang, führt ihn mit den Reads zusammen und legt das Ergebnis in den ProvenanceStore. Erzeugen tut er ihn nicht.
Der Materializer ist in diesem Bild optional. Ein Provider, der eine Query direkt beantwortet — etwa der Parser für ein AST —, erreicht ihn gar nicht.
Ob der ProvenanceStore physisch in derselben Struktur liegt, ist Implementierungsdetail. Zum Wissensbestand gehört er.
Drei qualitativ verschiedene Sorten Wissen liegen damit nebeneinander:
Cache vorläufig berechnete Query-Ergebnisse, jederzeit verwerfbar
DescriptorStore kanonische materialisierte Semantik, unveränderlich
ProvenanceStore revisionsgebundene Erklärung von Ableitungen und ihren Ausgängen
Kürzer: Der Cache speichert Antworten, der DescriptorStore kanonische Semantik, der ProvenanceStore Begründungen. Und eine Begründung kann auch lauten, dass diese Frage keine gültige Antwort hat, weil Prüfung X an Voraussetzung Y scheiterte.
3.2 Cache und DescriptorStore sind nicht dasselbe¶
Das ist der wichtigste Unterschied im ganzen System, und er wird leicht überlesen, weil beide Ergebnisse halten.
| Cache | DescriptorStore | |
|---|---|---|
| Was es ist | eine Beschleunigung | eine Wahrheit |
| Wegwerfen | folgenlos, nur langsamer | unmöglich, andere referenzieren es |
| Ersetzen | ja, bei Neuberechnung | nie, es entsteht ein neuer Eintrag |
| Wer schreibt | die Query-Ausführung, nach dem terminalen Ausgang | ausschließlich der Materializer |
| Lebensdauer | so lange es nützt | für immer, im Rahmen des Laufs |
Ein Cache-Eintrag darf verschwinden. Rechnet man neu, kommt dasselbe heraus.
Ein Deskriptor darf nicht verschwinden. Er wird auch nie an Ort und Stelle ersetzt — bestehende Referenzen zeigten sonst auf etwas anderes als das, was sie meinten.
Was bei einer Neuberechnung passiert, entscheidet das Interning (§4.5):
gleicher semantischer Inhalt → gleicher contentHash → vorhandene Id, kein neuer Eintrag
anderer semantischer Inhalt → neuer Hash → neuer Eintrag, neue Id
Daher auch die Schreibregel: In den DescriptorStore schreibt niemand direkt.
In den Cache schreibt ebenfalls nicht der Provider — er kennt seinen eigenen Ausgang gar nicht, wenn der Materializer ihn später ablehnt. Geschrieben wird, wenn die Ableitung terminierend entschieden ist:
3.3 Den DepGraph schreibt niemand¶
Eine Engine deklariert ihre Abhängigkeiten nicht. Sie erzeugt sie durch Lesen.
Jede Query, die ein Provider während seines Laufs stellt, wird mitgeschrieben. Daraus entsteht die Kante. Das ist der Grund, warum die Lesesicht eines Providers eingeschränkt sein muss — was an ihr vorbei gelesen würde, stünde nicht im Graphen und würde bei einer Änderung nicht invalidieren.
Ein Provider, der heimlich eine Datei liest oder die Uhr befragt, ist deshalb kein Stilproblem, sondern ein Korrektheitsproblem.
3.4 Der SeedStore¶
Die Einträge, die vor allem anderen existieren: die primitiven Typen, Option und Result, die Intrinsic-Tabelle, die Ring-0-Fakten.
Sie werden als Input gesetzt, nicht berechnet. Es gibt keinen Provider dafür — es gibt eine Zig-Tabelle, die beim Prägen der VM entsteht.
Der Seed ist der einzige Punkt im ganzen System, an dem noch keine VM läuft. Warum das so ist, steht in Abschnitt 6.3.
3.5 Der Materializer¶
Die einzige Instanz mit Schreibrecht auf den DescriptorStore — nicht auf den ProvenanceStore. Die Erklärung entsteht an zwei Orten (§3.6). Was er prüft, bevor er etwas ablegt:
PatchSet von einer Engine
|
v
Ring-Ebene: Ring-0-Fakten unangetastet? --- nein --------+
| |
| ja |
v |
Kohärenz: Provide schon vorhanden? -------- Konflikt ----+
| |
| ok |
v |
Konflikt: widersprechen sich Patches? ----- ja ----------+
| |
| nein |
v |
Constitution: OnDescriptorCommit ---------- verletzt ----+
| |
| ok v
v verwerfen
atomar committen
Fällt eine Prüfung durch, wird das ganze Patchset verworfen. Es gibt keinen Zwischenzustand, in dem ein Typ existiert, sein Meta aber noch fehlt.
Beim Committen schreibt der Materializer auch den Kopf des Deskriptors (§4.9). Den Inhalt liefert die Engine; die Felder, die jeden Deskriptor identifizieren, setzt nur er.
3.6 Wer die Erklärung schreibt¶
Ein DerivationRecord hat drei Bestandteile, die an zwei Stellen entstehen.
Provider-Auswertung
|
+--> CompilerDB zeichnet Reads auf --------------------->+
| |
v |
Provider erfolgreich? |
| |
+-- nein --> Failed(Provider), kein patchSet ----------->+
| |
+-- ja ----> PatchSet ---------------------------------->+
| |
v |
Materializer (prüft und entscheidet) |
| |
+--> DescriptorStore (kanonische Semantik) |
| |
+--> Resolved oder Failed(Materializer) -->+
|
v
DerivationRecord
|
v
ProvenanceStore
Reads und patchSet entstehen während der Provider-Auswertung. Die CompilerDB zeichnet jede Query auf; daraus werden gleichzeitig die Abhängigkeitskanten und ein Teil der Erklärung. Ein patchSet gibt es nur, wenn der Provider durchkam.
Der Ausgang entsteht dort, wo die Ableitung terminierend entschieden wird, und das ist nicht immer dieselbe Stelle:
Der DerivationRecorder ist damit die Stelle, die Reads und terminalen Ausgang zusammenführt — unabhängig davon, wer den Ausgang erzeugt hat.
Erst beides zusammen ergibt einen DerivationRecord. Der Materializer allein könnte ihn nicht schreiben; er weiß nicht, was der Provider gelesen hat.
Damit bleiben die Zuständigkeiten sauber: Der Materializer entscheidet Gültigkeit und schreibt Semantik. Die Erklärung des gesamten Ableitungswegs gehört der CompilerDB.
Und deshalb ist eine gescheiterte Ableitung kein Sonderfall. Sie hat Reads und einen Ausgang; ob sie ein patchSet hat, hängt davon ab, wie weit sie gekommen ist. Der Record entsteht auf demselben Weg.
3.7 Fragen als Daten — der Schnitt zwischen JDL und Zig¶
Status: Arbeitsrichtung. Die Grundidee ist gesetzt, der Umfang nicht. Offene Punkte stehen in §11.
Bisher stellt eine Meta-Funktion viele kleine Fragen und setzt die Antworten in JDL-Code zusammen. Dieser Abschnitt beschreibt den umgekehrten Weg: JDL formuliert eine ganze Frage als Wert, und Zig beantwortet sie.
3.7.1 Der Schnitt¶
Das Vorbild ist die Trennung zwischen einer Anwendung und ihrer Datenbank. Die Anwendung sagt, was sie wissen will. Wie die Datenbank die Antwort beschafft — über welchen Index, in welcher Reihenfolge, aus welchem Cache —, sieht die Anwendung nie.
Übertragen auf Jade:
JDL: Meta-Funktion
|
| baut die Frage mit einem Builder (jdl::, leere Row)
v
Query-Record (jade::, reine Daten)
|
| db.ask
v
=============================== Schnitt =======
Zig: CompilerDB
|
+--> liest das Schema des Inhalts (§3.7.4)
|
+--> nutzt Suchwege, filtert, verknüpft
|
+--> schreibt die Abhängigkeiten mit
|
v
Antwort (Daten)
|
v
JDL: Meta-Funktion arbeitet mit der Antwort weiter
Über den Schnitt geht in beide Richtungen nur Daten. Nach unten eine Frage, nach oben eine Antwort.
Die tragende Regel: Ring 0 wertet nur Daten aus, nie Code.
Deshalb ist zum Beispiel ein Filter immer ein Wert aus einer festen Liste, nie eine JDL-Funktion. Müsste Zig eine JDL-Funktion aufrufen, um eine Frage zu beantworten, wäre der Schnitt wieder weg.
Es gibt auch Vorbilder außerhalb von Compilern: Suchmaschinen und Dokumentdatenbanken lassen Abfragen als verschachtelte Datensätze formulieren statt als Text. Suchwort: Query-Dokument.
3.7.2 Der Query-Record ist der Vertrag¶
Alles, was Zig beantworten kann, steht in einem einzigen Typ. Eine Skizze:
// spekulativ
type Query : union {
fieldsOf: { target: TypeSubject, where: FieldFilter = .all }
variantsOf: { target: TypeSubject }
provideOf: { protocol: ProtocolSubject, args: [TypeSubject] }
}
- Zeile 2:
Queryist einunion. Jede Variante ist eine Art Frage. - Zeile 3: „Welche Felder hat dieser Typ?“ — optional mit Filter; ohne Angabe gilt „alle“.
- Zeile 4: „Welche Varianten hat dieser Typ?“ Diese Frage hat keinen Filter, also kann man auch keinen angeben.
- Zeile 5: „Gibt es ein Provide dieses Protokolls für diese Typen?“
Widerspruch seit 2026-10-05: Ein
union-Block nennt jetzt bestehende Typen (jdl-sprachreferenz§5.3). Varianten mit eigenem Namen und eigener Nutzlast, wie in der Skizze, sind die Bauart einesenum. Die Skizze wäre demnach entweder einenummit Record-Nutzlast oder eineunionüber eigene Frage-Typen, etwaFieldsOf,VariantsOf,ProvideOf. Welche Form, ist offen. Der Arbeitsname „Query-Union“ hängt daran.
Was das begrenzt: Eine Frage, die Zig nicht beantworten kann, lässt sich gar nicht hinschreiben. Und weil jede Variante nur ihre eigenen Felder hat, lassen sich auch keine unsinnigen Kombinationen bauen. Suchwort: make illegal states unrepresentable.
Was es nebenbei schenkt:
- Die Frage ist ihr eigener Cache-Schlüssel. Ein Query-Record ist serialisierbar und hat einen Hash. Gleiche Frage, gleicher Hash, gleiche Antwort.
- Jede Frage endet. Ein Record-Wert ist ein endlicher Baum. Wenn jede einzelne Operation in Zig endet, endet auch jede zusammengesetzte Frage. Rekursion gibt es auf dieser Ebene nicht; sie bleibt in JDL-Providern, die über die Datenbank rekursieren (§5.4).
Wo er liegt: in jade::. Er sieht aus wie Bibliothekscode, ist aber der Vertrag mit Ring 0.
3.7.3 Der Builder ist die Vorderseite¶
Den Record baut man nicht von Hand, sondern über einen Builder. Jede Methode nimmt eine Frage und gibt eine neue, erweiterte Frage zurück.
// spekulativ
def withProvide(q: FieldsQuery, protocol: ProtocolSubject) ->{} FieldsWithProvideQuery
- Die Row ist leer. Die Methode baut nur einen Wert und kommt gar nicht an die Datenbank.
- Der Rückgabetyp wechselt. Aus einer Frage nach Feldern wird eine Frage nach Feldern mit Provide. Damit weiß der Typ, wie die Antwort aussehen wird.
FieldsQuery und FieldsWithProvideQuery sind Typen des Builders in jdl::; jeder verpackt einen Wert von Query. Damit Zig „Felder mit Provide“ in einem Gang beantworten kann, muss diese Verknüpfung im Query-Record selbst vorkommen, als eigene Variante oder als allgemeine Operation. Die Skizze in §3.7.2 zeigt sie nicht; welche Form, ist offen (§11, „Umfang des Query-Records“).
Benutzt sieht das so aus:
// spekulativ
def missingCodecs(t: TypeSubject) ->Meta{CompilerDb} [Field] {
val entries = db.ask(
Query.fieldsOf(t)
.where(.notMeta("wire_skip"))
.withProvide(Codec)
)
var missing: [Field] = []
for entry in entries {
if entry.provide == None { missing.push(entry.field) }
}
ret missing
}
- Zeile 2: eine Meta-Funktion.
CompilerDbin der Row erlaubt ihr Fragen an die Datenbank. - Zeilen 3–7: eine Frage in drei Schritten: die Felder von
t, ohne die mitwire_skip, jeweils zusammen mit ihrem Codec-Provide. Erstdb.askschickt sie los. - Zeile 8: eine leere Liste für Felder ohne Codec.
- Zeilen 9–11: geht die Antwort durch. Jeder Eintrag hat
fieldundprovide, so wie der Typ der Frage es versprochen hat. - Zeile 12: gibt die Liste zurück.
Warum das mehr ist als Bequemlichkeit:
ohne Verknüpfung mit Verknüpfung
JDL --- Felder von T? ----------> Zig JDL --- Felder von T mit Codec? ---> Zig
JDL --- Codec für Feld 1? ------> Zig
JDL --- Codec für Feld 2? ------> Zig eine Frage,
JDL --- ... Zig verknüpft selbst
JDL --- Codec für Feld n? ------> Zig
n + 1 Fragen 1 Frage
Aus der Datenbankwelt ist das als N+1-Problem bekannt. Außerdem sieht Zig bei einer einzigen Frage genau, welche Fakten sie gelesen hat, und kann entsprechend genau invalidieren.
Wo er liegt: in jdl::. Der Builder ist reine Bequemlichkeit und kann Begriffe anbieten, die Anfänger verstehen, auch wenn der Record darunter allgemeiner ist.
Die Regel dazu: Builder-Funktionen haben eine leere Row. Stellte eine harmlos aussehende Methode heimlich eigene Fragen, stünden die weder in der Frage, die Zig sieht, noch dort, wo man sie erwartet. Über die Row lässt sich das prüfen, ohne dass jemand es durchsetzen muss.
3.7.4 Schema als Daten¶
Damit Zig eine Frage wie „Felder ohne wire_skip“ selbst beantworten kann, muss es in den Inhalt eines Deskriptors hineinschauen können. Der Inhalt ist aber eine kanonische Bytefolge, deren Aufbau die Engine festlegt, die ihn geschrieben hat.
Ohne weitere Hilfe bräuchte Zig für jede neue Rolle neuen Code, der genau dieses Format versteht. Das wäre das Gegenteil von erweiterbar.
Die Hilfe steht schon im Kopf: das Feld schema (§4.9). Heute sagt es nur, dass ein Format gilt. Es soll zusätzlich beschreiben, wie das Format aufgebaut ist — welche Felder es gibt, welchen Typ sie haben, wo sie liegen —, und zwar als Daten.
+------------------------------+
| Deskriptor (Rolle: Codec) |
| Kopf: hash, role, schema ---+-----+
| Inhalt: Bytefolge | |
+------------------------------+ |
v
+----------------------------+
| Deskriptor (Rolle: Schema) |
| Feld target: TypeRef |
| Feld format: WireFormat |
| Feld entries: [FieldRef] |
+----------------------------+
|
v
Zig kann den Inhalt lesen,
ohne zu wissen, was ein Codec ist
Datenbanken machen das seit Jahrzehnten so: Die Beschreibung der Tabellen liegt selbst als Tabelle in der Datenbank. Suchwort: Systemkatalog.
Daraus folgen zwei Dinge:
Schemas sind selbst Einträge im Store. Ein Schema ist ein Deskriptor mit der Rolle „Schema“, und schema im Kopf verweist über dessen Hash darauf. Eine neue Rolle bringt ihr Schema mit, und Zig kann sie sofort lesen.
Suchwege werden angemeldet statt fest eingebaut. Heute gibt es genau zwei fest verdrahtete: vom Hash zur Id und vom Namen zur Id (§4.5). Für Verknüpfungen braucht es mehr, etwa „alle Provides für dieses Protokoll“. Solche Suchwege meldet ein Schema an, und Zig baut und pflegt sie generisch.
Weil der DescriptorStore nur wächst und nie etwas löscht, ist das Pflegen für Zig vergleichsweise einfach: Ein Suchweg wird ergänzt, nie aufgeräumt.
3.7.5 Feste und revisionsabhängige Fakten¶
Nicht alles, wonach man fragt, steht fest im Store. Das ist der Punkt, an dem man sich bei diesem Ansatz am leichtesten vertut.
Feste Fakten betreffen den Inhalt eines Deskriptors: Welche Felder hat dieser TypeDescriptor? Die Antwort ändert sich nie, weil Deskriptoren unveränderlich sind.
Revisionsabhängige Fakten betreffen die Frage, welcher Deskriptor gerade gilt: Gibt es für T ein Codec-Provide? Ändert jemand den Quelltext, kann die Antwort eine andere sein. Der alte Deskriptor bleibt trotzdem im Store — der wächst nur.
Revision 1 Revision 2 (Provide im Quelltext gelöscht)
DescriptorStore DescriptorStore
+---------------------------+ +---------------------------+
| Codec-Provide für T | | Codec-Provide für T | <- liegt noch da
| TypeDescriptor T | | TypeDescriptor T |
+---------------------------+ +---------------------------+
"Gibt es ein Codec-Provide "Gibt es ein Codec-Provide
für T?" -> ja für T?" -> nein
Die Antwort rechts ist nein, obwohl der Eintrag noch im Store liegt. Eine Verknüpfung, die blind über den Store liefe, würde ja sagen — und damit eine Antwort aus einer Revision liefern, die nicht mehr gilt.
Die Regel: Feste Fakten darf Zig direkt aus dem Store lesen. Revisionsabhängige Fakten nur über die Queries der aktuellen Revision — und dabei entstehen Abhängigkeitskanten wie bei jeder anderen Query.
Trusted Core: Zig muss beide Sorten unterscheiden können, und zwar für jede Operation im Query-Record. Wie diese Unterscheidung im Schema oder im Query-Record ausgedrückt wird, ist offen (§11).
4. Was ein Deskriptor ist¶
4.1 Das Kriterium¶
Es gibt viele Dinge, die auf …Descriptor enden. Ein Deskriptor im eigentlichen Sinn erfüllt vier Bedingungen, alle gleichzeitig:
1 Es gibt ein Subjekt oder einen Blueprint, aus dem er entsteht.
2 Es gibt eine Engine, die ihn materialisiert.
3 Eine tiefere Schicht konsumiert ihn — Ring 0, VM, JME, Scheduler, Generator.
4 Er ist nach dem Commit unveränderlich und kanonisch serialisierbar.
Wer eine davon reißt, ist etwas anderes: ein Query-Ergebnis, ein Konfigurationsrecord, ein Profil oder ein Domänenobjekt.
Nützlich ist das Kriterium vor allem beim Aufräumen. Ein IrModule etwa entsteht nicht aus einem Blueprint über eine Engine — es ist ein Artefakt, kein Deskriptor, auch wenn es im selben Speicher landet.
Nicht jeder Eintrag der CompilerDB ist ein Deskriptor. Dort liegen auch Query-Ergebnisse, Seeds, Diagnosen, Semantic Views und Artefakte. Jeder Deskriptor ist ein Eintrag der CompilerDB; umgekehrt gilt das nicht.
Kurz gesagt: Ein Deskriptor ist ein geprüftes Versprechen an eine tiefere Schicht. Ein gewöhnlicher Eintrag ist Wissen des Compilers über sich selbst.
Widerspruch:
jade-compiler-db-spec§3.7 zähltIrModule,LoweredModule,ProtoundVmModuleArtifactzu den Deskriptoren. Nach dem Kriterium hier sind mindestensIrModuleundVmModuleArtifactArtefakte. Die Liste in §3.7 ist älter als das Kriterium und muss nachziehen.
4.2 Drei Ebenen vom Compilerwissen zur Laufzeit¶
Zwischen dem, was der Compiler weiß, und dem, was ausgeführt wird, liegen drei Ebenen und zwei Übergänge, an denen etwas passiert.
Compilerwissen
├── kanonische Semantik DescriptorStore
└── Ableitungsbegründungen ProvenanceStore
↓ Selektion und Projektion
Artefakt
└── ausführungsnotwendige Semantik, als Excerpts
↓ Laden und Zulassen
Laufzeit
└── materialisierte Ausführung
Der ProvenanceStore ist dabei keine Repräsentation eines Deskriptors. Er kann gerade dann existieren, wenn keiner entstanden ist (§4.5). Er gehört zum Compilerwissen, nicht zur Kette eines einzelnen Eintrags.
Blueprint oder Typsubjekt
|
v
Engine prüft, Materializer committet
|
v
+-- 1 Compilerwissen -------------------------------------------+
| |
| DescriptorStore ProvenanceStore Identität |
| semanticPayload ProofTrace, Spans DescriptorId |
| Meta, Layout, Origins, Diagnostik Arena-Index, |
| Refinements lauf-lokal |
| Nachschlagen über |
| Hash oder Name |
+----------------------------------------------------------------+
|
v
Selektion, Projektion
(Inhalt wird gefiltert, Verweise auf Excerpt-Hashes umgestellt)
|
v
+-- 2 Artefakt -------------------------------------------------+
| |
| Excerpts Identität |
| Kopf, gefilterter Inhalt contentHash je Excerpt, |
| Verweise über Hash stabil über Läufe hinweg |
+----------------------------------------------------------------+
|
v
Laden und Zulassen
(Verweise auf Laufzeit-Ids abbilden, Bindungen herstellen)
|
v
Laufzeit (Ebene 3): nur noch gelesen, keine TypeEngine
Im Compilerwissen liegt alles — aber in zwei getrennten Speichern (§4.5). Der DescriptorStore hält die Semantik, der ProvenanceStore die Erklärung: Proof Traces, Source-Spans, Herkunft. Beides braucht der Compiler, nur für Verschiedenes.
Im Artefakt liegt eine kontrollierte Teilmenge: nur die ausgewählten Deskriptoren, und von jedem nur, was die Laufzeit braucht (§4.4).
Zur Laufzeit wird nur noch gelesen. Es gibt keine laufende TypeEngine, die etwas ableiten könnte.
Streng genommen existiert ein Deskriptor damit nur in der CompilerDB. Im Artefakt und zur Laufzeit gibt es seinen Excerpt.
4.3 Drei Identitätsformen¶
DescriptorId Arena-Index, lauf-lokal, NICHT build-stabil
je Deskriptorart typisiert, etwa TypeId, LayoutId (§4.5)
contentHash kanonischer Hash des Inhalts, stabil
FQN stabiler Name, im Kopf als canonical, optional
Das ist der folgenreichste Repräsentationsfakt im ganzen System.
Innerhalb eines Laufs adressiert man über die Id — das ist ein Index, also billig. Über Laufgrenzen hinweg nie: Die Id wird in Anlagereihenfolge vergeben, und die ändert sich, sobald jemand eine Datei umsortiert.
Was über Läufe trägt, ist der contentHash oder der vollqualifizierte Name. Deshalb tauchen beide an so vielen Stellen auf — im Seed, im persistenten Cache, im Plugin-Fingerabdruck, in der Sortierregel für Erzeuger.
Die drei sprechen denselben Eintrag an, beantworten aber verschiedene Fragen.
Die Id steht deshalb nicht im Kopf eines Deskriptors (§4.9). Sie würde beim nächsten Lauf nicht mehr stimmen.
Verbunden sind die drei über zwei Nachschlagewege (§4.5): vom Hash zur Id und vom Namen zur Id. Die Suche über den Hash findet höchstens einen Eintrag, denn gleicher Hash heißt gleicher Inhalt. Die Suche über den Namen findet anonyme Einträge gar nicht.
Nicht jeder Deskriptor hat einen Namen. Ein inline gebauter Blueprint ohne eigene Typdefinition hat nur einen Hash. Ihm einen künstlichen Namen zu geben, wäre versteckter Automatismus. Weil Verweise über den Hash laufen (§4.5), fehlt ihm dadurch nichts.
Daraus folgt eine Regel: Was exportiert wird, braucht einen Namen. Nachschlagen kann man nur, was einen Namen hat.
Bei benannten Deskriptoren gehört der Name zum Inhalt und geht in den contentHash ein. Sonst hätten zwei Typen mit gleicher Struktur, aber verschiedenem Namen denselben Hash, und Ring 0 könnte sie nicht unterscheiden. Getrennt benannt sind sie aber gerade, damit man sie nicht verwechselt.
Bei anonymen Deskriptoren geht nur der Inhalt ein. Zwei anonyme mit gleichem Inhalt sind derselbe Eintrag.
4.4 Selektion und Projektion sind einseitig¶
Der Übergang vom Compilerwissen zum Artefakt besteht aus zwei Schritten.
Selektion wählt aus, welche Deskriptoren mitgehen: alles, was von den Wurzeln des Artefakts über Verweise erreichbar ist. Wurzeln sind die Einstiegspunkte und die Exporte. Eine Bibliothek hat keinen Einstiegspunkt, aber Exporte. Jeder Export nimmt außerdem den Rumpf mit, der auf ihn verweist (§4.9).
Projektion wählt aus, was von jedem ausgewählten Deskriptor mitgeht. Bei Typen entscheidet reified darüber.
Das Ergebnis der Projektion für einen einzelnen Deskriptor heißt Excerpt. Das Artefakt ist das Paket aus allen ausgewählten Excerpts.
Suchwort: relationale Algebra. Dort heißt das Auswählen von Zeilen Selektion und das Auswählen von Spalten Projektion — dieselbe Aufteilung.
| bleibt im Compiler | geht in den Excerpt |
|---|---|
| Meta-Record | Kopf (§4.9) |
| Proof Trace | typeRef, kind |
| AST, Source Spans | Feld- und Variantentabelle bei Serde-Bedarf |
| Constitution-Regeln | Layout-, Drop-, Dispatch-, Codec-Referenzen |
| Refinements, Provider-Interna | Flags |
| Herkunft, auch die bauende Engine | qualifizierter Name: bei Exporten Pflicht, sonst optional |
Der Kopf überlebt die Projektion. Ohne schema wüsste der Empfänger nicht, wie er den Excerpt lesen soll.
Ein Excerpt trägt seinen eigenen Hash. Er wird nach derselben Regel berechnet wie der contentHash (§4.9), nur über das, was im Excerpt steht. Das ist das, was ankommt und bei der Zulassung geprüft wird. Der Hash des Deskriptors, aus dem er entstand, ist Herkunft und bleibt im Compiler.
Im Excerpt zeigt ein Verweis auf den Excerpt-Hash seines Ziels. Im Compiler zeigt er auf den contentHash des Ziel-Deskriptors (§4.5). Dieser Hash verlässt den Compiler aber nicht; im Artefakt fände ein solcher Verweis nichts. Die Projektion stellt deshalb jeden Verweis auf den Excerpt-Hash des Ziels um.
Dafür rechnet sie von unten nach oben: zuerst die Excerpts, die auf nichts verweisen, dann jene, deren Ziele schon berechnet sind. An der Bedeutung ändert das nichts. Der Verweis zeigt auf dasselbe Ding, nur in der Sprache des Artefakts.
Das hat eine gewollte Folge: Ändert sich an einem Deskriptor nur etwas, das die Projektion wegfiltert, bleibt sein Excerpt-Hash gleich — und damit auch der Hash jedes Excerpts, der auf ihn zeigt.
Trusted Core: Die Zulassung rechnet jeden Excerpt-Hash nach, mit exakt derselben Regel wie der Compiler. Weicht die Regel an einer Stelle ab, passt kein Hash.
Umgebungsabhängige Werte stehen nicht im Artefakt. Welche Datenbank, welcher Socket, welches Geheimnis: Das kommt erst bei der Zulassung über Bindungen dazu (wie-jade-ausfuehrt §11). Im Artefakt sind diese Bindungen offen.
Dadurch läuft dasselbe Artefakt mit demselben Hash auf dem Laptop und in Produktion. Und Geheimnisse landen nie im Build.
Aus dem Excerpt lässt sich der Deskriptor nicht rekonstruieren. Das ist keine Einschränkung, sondern die Zusage: Wer ein Artefakt bekommt, kann daraus nicht ableiten, wie das Programm entstanden ist.
Und es ist zugleich der Grund, warum zur Laufzeit keine TypeEngine läuft — das Material dafür ist gar nicht vorhanden.
Debug- und REPL-Bündel dürfen mehr mitnehmen. Das ist dann ein anderes Artefakt, nicht dasselbe mit einem Zusatzschalter.
4.5 Wie der Store aussehen kann¶
Die Spec legt das Layout nicht fest. Was sich aus den Eigenschaften ergibt:
Drei Teile, nicht einer.
Arena die Einträge selbst, unveränderlich, nie freigegeben
Interning-Map contentHash → Id
Namensindex FQN und Revision → Id
Nur der erste Teil ist Speicher. Die beiden anderen sind Zugriffswege und beantworten verschiedene Fragen: „Gibt es das schon?" und „Wie heißt das?"
Semantik und Herkunft liegen getrennt. Das ist die Voraussetzung dafür, dass das Interning überhaupt trägt:
DescriptorStore
└── DescriptorId
└── semanticPayload geht in den contentHash
Layout · Felder · Varianten · Policies · Referenzen
bei benannten Deskriptoren auch der Name (§4.3)
ProvenanceStore
└── DerivationKey geht NICHT in den Hash
└── DerivationRecord reads · patchSet · outcome · materialized
Daraus wird eine allgemeine Identitätsregel, die über Deskriptoren hinausreicht:
Semantische Identität ist nicht dasselbe wie die historische Erklärung dieser Identität.
Der semanticPayload bestimmt Gleichheit, Hash und Interning. Die Provenance erklärt die Entstehung und darf die Gleichheit nicht sabotieren.
Warum die Herkunft nicht im Eintrag liegen darf, folgt direkt aus dem Interning. Zwei Ableitungswege können denselben Inhalt liefern — dann bekommen sie dieselbe Id. Läge die Herkunft im Eintrag, gewänne entweder die erste und die zweite verschwände, oder man müsste einen unveränderlichen Eintrag nachträglich ergänzen. Beides geht nicht.
Aus demselben Grund steht auch die bauende Engine nicht im Kopf (§4.9). Zwei Engines können denselben Inhalt liefern.
Also ist Provenance mehrwertig. Dasselbe Layout kann aus zwanzig verschiedenen Typen folgen — es ist identisch, aber die Erklärung, warum Typ A es bekam, ist nicht die von Typ B.
Und der Schlüssel hängt nicht am Deskriptor. Das ist die Ausrichtung, die man leicht falsch herum wählt: Provenance ist keine Beilage zu einem Eintrag, sondern die Geschichte einer Ableitung — und eine Ableitung kann auch scheitern. Bei einer abgelehnten gibt es gar keine DescriptorId, an der sie hängen könnte.
Also eine Ebene tiefer:
DerivationKey = Subject/Query + Revision + Origin
DerivationRecord = reads
patchSet: Option[PatchSet]
outcome: Resolved
| Failed(Provider, DiagnosticSet)
| Failed(Materializer, DiagnosticSet)
materialized: Option[DescriptorId]
Der Deskriptor steckt nicht im Ausgang, sondern in einem eigenen Feld. Eine erfolgreiche Ableitung muss keinen erzeugen: Der Parser-Provider aus §10 liefert ein AST, kein PatchSet und keinen Deskriptor — und ist trotzdem Resolved.
Ein Provider kann außerdem scheitern, bevor überhaupt ein PatchSet entsteht — die Codec-Engine in §7.1 wirft ihre gesammelten Diagnosen, bevor sie irgendetwas baut. Dann läuft der Materializer nie.
Vier Fälle, kein Sonderfall:
Parser-Provider erfolgreich reads · kein patchSet · Resolved · kein Deskriptor
DescriptorEngine erfolgreich reads · patchSet · Resolved · DescriptorId
Materializer lehnt ab reads · patchSet · Failed(Materializer) · kein Deskriptor
Provider scheitert vorher reads · kein patchSet · Failed(Provider) · kein Deskriptor
Cancelled ist ausdrücklich kein vierter Ausgang. Eine wegen neuer Revision abgebrochene Berechnung hat die Query nicht beantwortet. Sie ist ein operativ verworfener Versuch, keine Begründung — und gehört damit in die Telemetrie, nicht in die Provenance.
Das Feld heißt patchSet und nicht „angewandte Patches" — bei einem Fehlschlag wurde gerade nichts angewandt, und manchmal gibt es gar keines.
Fehlschläge sind cachebar. Ein Fehler ist ein Query-Ergebnis wie jedes andere: Seine Reads sind bekannt, also lässt er sich invalidieren wie ein Erfolg.
Resolved cachebar
Failed(Provider, …) cachebar
Failed(Materializer, …) cachebar
Cancelled nicht cachebar
Ohne das würde ein Sprachserver denselben kaputten Typ bei jeder abhängigen Anfrage erneut durchrechnen. Und Early Cutoff greift auch hier: Bleibt nach einer Änderung derselbe semantische Fehler bestehen, ist die fehlgeschlagene Query stabil und die Ungültigkeit propagiert nicht weiter.
Was dabei „derselbe Fehler" heißt, ist offen — und es ist dieselbe Frage wie bei der Deskriptor-Identität, weil Source-Spans nicht dazugehören sollten.
Was der Cache dadurch hält, ist der CompilerDB-QueryResult-Record einer beantworteten Query. Dessen data enthält die kanonisch serialisierte fachliche Antwort; bei Einzelwert-Queries ist das typischerweise JadeResult[T]:
Resolved beziehungsweise Failed(Provider|Materializer) bezeichnet dagegen den DerivationOutcome im DerivationRecord: also wie die konkrete Ausführung endete, nicht einen zweiten Query-Ergebnistyp. Cancelled erzeugt weder einen neuen QueryResult noch einen DerivationRecord, weil die Query unbeantwortet blieb.
Für die erfolgreichen Fälle kommt ein Index dazu:
Damit sind beide Fragen beantwortbar, und zwar mit demselben Bestand:
Das zweite ist für Werkzeuge das Wertvollere. Ein Compiler, der erklären kann, warum etwas funktioniert, beim Fehlerfall aber schweigt, wäre immerhin traditionsbewusst.
Die richtige Form der :why-Frage folgt daraus. Nicht „warum dieser Deskriptor", sondern „warum löst dieses Subjekt in dieser Revision auf diesen Deskriptor auf" — oder eben auf nichts.
Damit stehen drei Identitäten nebeneinander, die vorher alle am Deskriptor hingen:
Query-Identität Welche Frage wurde gestellt?
Derivation-Identität Welche konkrete Ableitung dieser Frage geschah
in Revision und Origin X?
Deskriptor-Identität Welche kanonische Semantik kam dabei heraus?
Ein Deskriptor ist damit ein möglicher Ausgang einer Ableitung, nicht ihr Zentrum.
Ohne die Trennung würde außerdem das Verschieben einer Typdefinition um zehn Zeilen ihren Hash ändern. Bei verpflichtendem Interning wäre das widersinnig: Dieselbe Semantik bekäme zwei Einträge, weil jemand eine Leerzeile eingefügt hat.
Drei Leitplanken, damit der ProvenanceStore nicht stillschweigend zum zweiten ewigen Archiv wird:
1. Der DescriptorStore ist semantisch, interned und innerhalb einer
CompilerDB-Instanz append-only.
2. Der ProvenanceStore ist erklärend, an Ableitungen gebunden statt an
Deskriptoren, revisionsgebunden und darf veralten, verworfen oder
kompakt gehalten werden.
3. :why bezieht sich ohne explizite Revisionsangabe auf die aktuelle
Revision.
Die zweite ist die wichtigste. Ohne ausdrückliche Erlaubnis zu veralten würde die Provenance die Unveränderlichkeit des DescriptorStore erben, einfach weil sie danebenliegt — und bei einem Sprachserver wären tausend Revisionen desselben Ableitungswegs tausend Erklärungen, die niemand mehr braucht.
Eine spätere Verdichtung über firstRevision und lastRevision pro Ableitungsweg ist damit möglich, aber Implementierungspolitik. Ob sie sich lohnt, hängt an einer Zahl, die noch niemand kennt: wie oft ein Weg über Revisionen hinweg unverändert bleibt.
Die dritte Leitplanke ist die unauffälligste und die, die man beim Implementieren am ehesten vergisst. Ohne sie müsste jeder Aufrufer eine Revision mitgeben — und dann wäre die naheliegendste Frage, „warum ist das jetzt so", die umständlichste.
Was der Hash über Referenzen sagen muss. Der semanticPayload enthält Verweise auf andere Deskriptoren. Würden dort lauf-lokale Arena-Ids gehasht, wäre der angeblich stabile contentHash nicht stabil.
contentHash = Hash der kanonischen Semantik
inklusive der Verweise, ausgedrückt als contentHash des Ziels
niemals CompilerDB-lokaler Arena-Ids
Ein Verweis ist der Hash seines Ziels. Der Name des Ziels dient nur zum Nachschlagen (§4.3); ob etwas passt, entscheidet der Hash.
Damit enthält der Hash eines Deskriptors die Hashes aller Deskriptoren, auf die er zeigt. Ändert sich tief unten etwas, ändern sich alle Hashes darüber. Suchwort: Merkle-Baum.
Innerhalb eines Artefakts ist diese Kettenreaktion gewollt. Dort läuft sie über die Excerpt-Hashes (§4.4). An Artefaktgrenzen hält sie an, weil Verweise dort nur auf Schnittstellen zeigen (§4.9).
Offen bleibt der Fall, in dem ein Deskriptor über Umwege auf sich selbst zeigt (§11).
Interning ist nicht optional. Weil Einträge unveränderlich sind und einen contentHash über ihren semantischen Teil tragen, ist Deduplizierung geschenkt. In einem Sprachserver mit tausend Revisionen entstehen kaum neue Deskriptoren — dieselbe Typform materialisiert immer wieder zum selben Inhalt.
Es verhindert damit redundantes Wachstum, nicht Wachstum durch tatsächlich neue Semantik. Ein append-only Store wächst weiter, solange neue Bedeutungen entstehen; nur eben nicht durch Wiederholung derselben.
Eine Arena pro Deskriptorart, mit eigener typisierter Id. Der Grund ist derselbe, aus dem TypeId, SymbolId und HandleId getrennte Typen sind: Eine Layout-Id, die versehentlich als Typ-Id durchgeht, ist ein Fehler, den der Compiler fangen sollte.
Heiße Felder, kalter Rumpf.
heiß feste Größe, direkt lesbar
der Kopf: hash · role · schema · canonical (§4.9)
dazu oft gelesene Felder des Inhalts, bei Typen etwa:
size · align · Registerklasse · Drop-Art · Flags · Kind
kalt kanonische Bytefolge in einer Nebenarena
Feldtabelle · Variantentabelle · Namen · Referenzen
Der Generator greift ständig auf size und align zu und sollte dafür nichts parsen müssen. Die Feldnamen fasst außer Werkzeugen und Serde niemand an.
Die heißen Felder des Inhalts gehören nicht zum Kopf. Sie sind Inhalt, den die Engine liefert, und liegen nur im Speicher neben dem Kopf, weil sie oft gebraucht werden. Das ist eine Frage des Speicherlayouts, nicht der Bedeutung.
canonical hat keine feste Größe. Im Kopf steht deshalb ein Verweis in den Namensindex, nicht der Name selbst.
4.6 Cache und Provenance speichern nicht dasselbe¶
Das ist der Punkt, an dem die ganze Trennung zusammenläuft. Drei Dinge, drei Fragen:
QueryResult Was soll der nächste identische Aufrufer bekommen? Cache
DerivationRecord Warum ist genau das die Antwort? ProvenanceStore
Deskriptor Welche kanonische Semantik wurde dabei materialisiert? DescriptorStore
Wann welches entsteht — und die Unterscheidung gilt für neu ausgeführte Ableitungen:
neu ausgeführt, Resolved QueryResult · DerivationRecord · ggf. Deskriptor
neu ausgeführt, Failed QueryResult · DerivationRecord
Cache-Hit vorhandenes QueryResult, sonst nichts
Input gesetzt QueryResult, kein Provider-DerivationRecord
Cancelled nichts wird publiziert
Ein Cache-Hit beantwortet die Query, leitet sie aber nicht ab. Er liefert ein bestehendes QueryResult und erzeugt keinen neuen Record — sonst wüchse die Provenance mit der Zahl der Anfragen statt mit der Zahl der Ableitungen.
Ein Input hat keinen Provider (§3.4). Er wird gesetzt, nicht berechnet. Ob er eine eigene Input-Provenance bekommt — wer ihn wann gesetzt hat — ist eine eigene Frage; ein Ableitungsrecord im Sinn dieses Abschnitts entsteht jedenfalls nicht.
Cancelled publiziert nichts — die Query blieb unbeantwortet. Damit ist ein Abbruch ein Betriebsereignis und kein Stück Compilerwissen.
Daraus folgt eine Grenze, die der Implementierung vorgegeben werden muss: Wäre ein Deskriptor bereits committet und der Abbruch würde erst danach bemerkt, stimmte die letzte Zeile nicht mehr. Also entweder liegt der letzte Prüfpunkt vor der Publikation und diese läuft atomar, oder ab Commit-Beginn ist keine Cancellation mehr zulässig. Eines von beidem muss gelten.
Daraus folgt etwas Praktisches: Ein gecachter Fehler darf weiterleben, nachdem seine ausführliche Provenance längst verworfen wurde. Die Query bleibt korrekt beantwortbar; nur :why verliert die Erklärung.
Das ist kein Defekt, sondern genau die Oberregel aus §4.8: Funktionsfähigkeit darf nicht von Erklärbarkeit abhängen. Hier zeigt sie sich innerhalb der CompilerDB selbst.
4.7 Der Proto ist dasselbe Muster für Funktionen¶
Ein TypeDescriptor beschreibt einen Typ. Für eine Funktion gibt es das Gegenstück, und es heißt Proto: code[], constPool[], regLayout, debugSideTable. Unveränderlich, von allen Ausführungen geteilt.
Die Type Engine liefert die Fakten — Typauflösung, Policies aus den Refinements, size, align, Feld-Offsets, und ob ein Wert als Bytes im Registerfile oder als HandleId liegt.
Der Generator entscheidet nichts über Bedeutung — insofern ist er reiner Leser. Er ist aber kein bloßer Proto-Emitter, sondern die Transformationsstufe des Compilers:
bereits geprüfte Semantik
|
v
Generator-Frontend
|
v
Jade-IR
|
v
Passes, Lowering, Verifikation
|
v
Backend
|
+--> JBC VM-nah, introspektierbar
|
+--> Cranelift nativ
|
+--> WebAssembly portabel
Der Proto ist die Ausgabe eines Backends, nämlich des JBC-Pfads. Die anderen erzeugen andere Repräsentationen derselben Semantik. Der Generator selbst ist ein Blueprint mit Engine wie alles andere: Ein CodeEmissionSpec beschreibt Emissionsregeln, Registerallokation, Passes und Ausgabeformat.
Der Generator ist auch die Stelle, die Selektion und Projektion (§4.4) ausführt. Er darf dabei nur filtern — was er an Bedeutung weglässt, fehlt; was er an Bedeutung verändern würde, wäre ein Fehler. Verweise auf Excerpt-Hashes umzustellen, verändert keine Bedeutung (§4.4).
Was daraus zur Laufzeit wird — der Frame — steht in wie-jade-ausfuehrt §7.
4.8 Die Regel darunter¶
Die Trennung von Semantik und Herkunft ist kein Einzelfall. Dieselbe Grenze taucht an fünf Stellen auf:
Bedeutung ≠ Erklärung ihrer Entstehung
Deskriptor ≠ Provenance
Runtime-Semantik ≠ Compilerwissen
Control Surface ≠ RuntimeBus
Artefakt ≠ CompilerDB
Dahinter steht eine einzige Regel:
Was zur Ausführung gebraucht wird, darf nicht davon abhängen, was zur Erklärung gebraucht wird.
Die Richtung ist wichtig. Es ist keine Wertung: Provenance ist nicht sekundär. Für Diagnose, Werkzeuge, Nachvollziehbarkeit und Vertrauen ist sie oft wertvoller als manches Laufzeitartefakt. Sie ist nur nicht ausführungsnotwendig.
Aus dieser einen Regel folgen Entscheidungen, die sonst einzeln begründet werden müssten:
Provenance geht nicht in den contentHash
die bauende Engine steht nicht im Kopf
ProofTraces stehen nicht im Laufzeitartefakt
:why wird zur CompilerDB geroutet, nicht zum RuntimeBus
der DescriptorStore überlebt eine Provenance-Löschung
die Laufzeit funktioniert ohne jedes Compilerwissen vollständig
4.9 Der Kopf eines Deskriptors¶
Deskriptoren gibt es in vielen Rollen. Sie haben trotzdem eine einheitliche Form:
Kopf einheitlich, vom Materializer geschrieben
hash Identität, immer vorhanden
role was für ein Deskriptor das ist
schema in welchem Format der Inhalt geschrieben ist
canonical Name zum Nachschlagen, optional
Inhalt von der Engine geliefert, je nach Rolle verschieden
Verweise auf andere Deskriptoren, immer über deren Hash
(im Excerpt über den Excerpt-Hash des Ziels)
hash ist der contentHash aus §4.3. Im Excerpt steht an dieser Stelle der eigene Hash des Excerpts (§4.4).
Was in den Hash eingeht: der Inhalt, bei benannten Deskriptoren der Name, dazu role und schema — derselbe Inhalt bedeutet unter anderer Rolle oder in anderem Format etwas anderes. Nie der Hash selbst: Ein Wert, der sich selbst enthält, lässt sich nicht berechnen. Der Kopf ist also nicht als Ganzes Teil des gehashten Inhalts, sondern nur mit diesen Feldern.
In den Kopf gehört nur, was ein Leser braucht, um den Eintrag lesen zu können. Was erklärt, woher der Eintrag kommt, gehört in den ProvenanceStore.
Die bauende Engine gehört nicht in den Kopf. Sie ist Herkunft (§4.5). Muss zur Laufzeit eine Engine etwas instanziieren, wie bei einem ContextDescriptor, dann ist sie ein gewöhnlicher Verweis im Inhalt. Dort ist sie Teil der Bedeutung, nicht der Verwaltung.
role ergibt sich nicht aus der Engine. Eine TypeEngine erzeugt mehrere Rollen: TypeDescriptor, LayoutDescriptor, LabelProfile.
schema ist die öffentliche Schnittstelle der Engine. Es sagt, wie der Inhalt aufgebaut ist. Ein Empfänger, der das Schema nicht kennt, lehnt ab, statt zu raten. Wie Schema-Versionen miteinander verglichen werden, ist offen (§11).
canonical ist kein Blueprint-Feld. Der Nutzer schreibt es nicht; der Materializer setzt es aus dem vollqualifizierten Namen. Lesbar ist es über die Meta-Sicht auf den Deskriptor.
Die Id steht nicht im Kopf (§4.3). Sie gilt nur in einem Lauf.
Verweise über Artefaktgrenzen zeigen nur auf Schnittstellen. Ein Artefakt, das ein anderes lädt, kennt vom anderen nur den Hash der Schnittstelle, nicht den der Implementierung.
Die Implementierung darf sich damit ändern. Solange der Schnittstellen-Hash gleich bleibt, passt sie. Ändert sich die Schnittstelle, ändert sich der Hash, und die Zulassung scheitert.
Dafür muss der Hash einer Schnittstelle allein von ihrer Signatur abhängen, nie von einem Rumpf.
Der Schnittstellen-Hash ist kein eigener Begriff. Er ist der Excerpt-Hash des Exports: Ein Export geht als eigener Excerpt ins Artefakt, mit Name und Signatur einschließlich Row, ohne Rumpf. Verweise von außen zeigen auf diesen Excerpt. Der Rumpf steht in einem anderen Excerpt, auf den nur das eigene Artefakt zeigt.
Der Rumpf zeigt auf seinen Export, nicht umgekehrt. Zeigte der Export auf den Rumpf, hinge sein Hash doch wieder am Rumpf. Also trägt der Rumpf-Excerpt einen gewöhnlichen Verweis auf den Export, den er umsetzt. Das ist dieselbe Richtung wie bei provide P for T: Die Implementierung nennt das Protokoll, nicht das Protokoll seine Implementierungen.
Der Hash des Exports bleibt damit unabhängig vom Rumpf. Der Hash des Rumpfs hängt am Export; das schadet nicht, denn ändert sich die Schnittstelle, ändert sich der Rumpf ohnehin. Die Zuordnung ist gewöhnlicher, gehashter Inhalt, keine Sonderstruktur. Wird ein Rumpf ausgetauscht, zeigt der neue auf denselben Export, und wer von außen darauf verweist, merkt nichts.
Woher die Exporte kommen, regelt die Constitution: Was sie freigibt, wird zum Export-Excerpt (spec-namespaces-und-module §4).
Trusted Core: Was beim Laden geprüft wird.
1 Verzeichnis Ring 0 liest die Verweise der Rümpfe umgekehrt und baut
daraus einmal das Verzeichnis „Export -> Rumpf“
2 genau einer jeder Export hat genau einen Rumpf; keiner oder zwei
sind ein Ladefehler
3 Signatur der Rumpf hat dieselbe Signatur wie sein Export;
sonst verspräche der Export etwas anderes, als läuft
Der Weg vom Namen zum Code ist damit: Name, Export, Hash-Vergleich, über das Verzeichnis zum Rumpf. An keiner Stelle führt der Name allein zum Code.
Wie Punkt 3 genau prüft, ist ein Vorschlag: Export und Rumpf verweisen beide auf denselben Excerpt der Signatur, dann ist die Prüfung ein einziger Hash-Vergleich. Die Alternative wäre ein Vergleich der Inhalte (§11).
Trusted Core: Diese Forderung trifft den Hash selbst, nicht nur eine Engine — eine Schnittstelle, deren Hash vom Rumpf abhinge, würde jedes Update ablehnen.
Die Zulassung prüft in zwei Schritten: über den Namen nachschlagen, dann den Hash vergleichen. Über den Namen allein wird nie etwas zugelassen — er liefert nur, was gerade unter diesem Namen liegt.
Es gibt damit keine eingebaute Verträglichkeitsprüfung, die zur Ladezeit entscheidet, ob „ungefähr passend" reicht. Passt der Hash, passt es; sonst nicht.
5. Engines¶
5.1 Eine Engine tritt im Compilerpfad als Provider auf¶
Das ist der Punkt, an dem für die meisten der Groschen fällt: Eine Engine ist kein Dienst, den man aufruft. Niemand ruft sie auf. Jemand fragt die CompilerDB nach einem Deskriptor — und wenn die Antwort fehlt, läuft sie als Provider dieser Query.
Liegt ein gültiges Ergebnis im Cache, kommt es von dort. Der Normalfall ist, dass sie nicht läuft.
Engine und Provider sind trotzdem keine Synonyme. Ein Provider ist eine Rolle im Query-Mechanismus; eine Engine ist eine Implementierung mit describe und optional instantiate. Die Laufzeithälfte einer RuntimeEngine ist gar kein Provider, und ein Provider muss keine Engine sein.
5.2 Der Ablauf¶
Aufrufer -> CompilerDB : GetTypeDescriptor(subject)
CompilerDB : Cache gültig?
|
+-- Treffer
| |
| +-- CompilerDB -> Aufrufer : Ergebnis
|
+-- Fehltreffer
|
+-- CompilerDB -> Engine : describe(subject)
+-- Engine -> CompilerDB : db.typeKind(t)
+-- CompilerDB -> Engine : Kind
+-- Engine -> CompilerDB : db.fields(of: t)
+-- CompilerDB -> Engine : Felder
| (jede Query erzeugt eine Abhängigkeitskante)
|
+-- Provider scheitert
| |
| +-- Engine -> CompilerDB : Diagnosen
| +-- CompilerDB : QueryResult in den Cache, Kanten in den DepGraph
| +-- CompilerDB : DerivationRecord: Failed(Provider)
| +-- CompilerDB -> Aufrufer : Diagnose
|
+-- Provider liefert
|
+-- Engine -> CompilerDB : PatchSet
+-- CompilerDB -> Materializer : committen
+-- Materializer : Ring, Kohärenz, Konflikt, Constitution
|
+-- abgelehnt
| |
| +-- Materializer -> CompilerDB : Diagnosen
| +-- CompilerDB : QueryResult in den Cache, Kanten in den DepGraph
| +-- CompilerDB : DerivationRecord: Failed(Materializer)
| +-- CompilerDB -> Aufrufer : Diagnose
|
+-- committet
|
+-- Materializer -> CompilerDB : Id
+-- CompilerDB : QueryResult in den Cache, Kanten in den DepGraph
+-- CompilerDB : DerivationRecord: Resolved
+-- CompilerDB -> Aufrufer : Ergebnis
5.3 Sie gibt einen Patch zurück, keinen Deskriptor¶
Das ist die Stelle, die den ganzen Mechanismus trägt.
Eine Engine liefert eine Änderungsabsicht ab. Ein Patch sagt: Lege diesen Typ an, setze dieses Meta, registriere dieses Provide. Er hat noch keine Id, keinen Platz im Speicher und keine Gültigkeit.
Er hat auch noch keinen Kopf. Den schreibt der Materializer beim Commit (§4.9).
Damit kann eine Engine, die auf halbem Weg scheitert, nichts halb Fertiges hinterlassen haben. Sie hat nirgends geschrieben.
Und deshalb braucht sie auch keinen Zustand, keine Aufräumarbeit und keine Fehlerbehandlung für den Abbruch. Ein Abbruch mitten in einer Schleife hinterlässt nichts.
Bei einer RuntimeEngine sieht das von außen anders aus, ist aber dasselbe (jdl-blueprint-spec §9.1, Vorschlag). Ihr describe liefert den Inhalt eines Deskriptors als getypten Wert — den, den instantiate später als Excerpt liest. Die CompilerDB verpackt diesen Wert in einen Patch, und der Materializer schreibt ihn samt Kopf. Auch hier schreibt die Engine also nirgends selbst; sie liefert nur einen Wert ab, der lesbarer ist als ein allgemeines PatchSet.
5.4 Rekursion läuft über die Datenbank¶
Ein Struct mit einem Struct-Feld braucht einen Codec für den inneren Typ. Die naheliegende Lösung wäre, die eigene Funktion rekursiv aufzurufen.
Das wäre falsch, und zwar aus drei Gründen gleichzeitig:
Über den Stack:
Codec bauen <-------+
| |
v |
Codec bauen | Zyklus
| |
v |
... ----------------+
Über die Datenbank:
Codec bauen
|
v
Query an die CompilerDB
|
+--> Kante entsteht
|
+--> zweiter Aufruf: Cache
|
+--> Zyklusprüfung der DB
Über den Stack entstünde keine Abhängigkeitskante, nichts wäre gecacht, und ein zyklischer Typ schickte den Compiler in eine Endlosschleife.
Über die Datenbank passiert alles drei automatisch.
5.5 Zwei Protokolle¶
Das zweite enthält das erste. Jede Engine hat ein describe in der Meta-Phase; manche haben zusätzlich ein instantiate zur Laufzeit.
nur describe ConstitutionEngine · TypeEngine · CodecEngine
erzeugen Wahrheit, laufen nie zur Laufzeit
beides HttpEngine · ActorEngine · TestEngine · ContextEngine
beschreiben eine Absicht und betreiben sie später
nur instantiate gibt es nicht
Der letzte Punkt ist kein Zufall. Eine Engine ohne describe hätte nichts, worauf sie sich stützt — sie müsste zur Laufzeit ableiten, was der Compiler hätte prüfen sollen.
Die ContextEngine gehört in die zweite Gruppe: describe baut in der Meta-Phase den ContextDescriptor, instantiate erzeugt zur Laufzeit aus dessen Excerpt die laufende Domäne (wie-jade-ausfuehrt §5.5).
Was instantiate an Services braucht, steht in einem Row-Platz des Protokolls, den jede Implementierung über ihre Signatur füllt (jdl-effect-rows-und-throw §4.5, jdl-blueprint-spec §9.1). Eine Row-Variable an instantiate gibt es nicht mehr.
5.6 Vier Ebenen des Wortes „Engine"¶
EngineDefinition (JDL-Code, hat nie Zustand)
|
| describe
v
EngineDescriptor (geprüfter Vertrag; im Artefakt als Excerpt)
|
+-------------------------------+
| |
v |
EngineRuntimeConfig | instantiate, liest den Excerpt
(Limits, Timer, mechanische |
Runtime-Parameter) |
| |
v v
EngineInstance (lebt, hat Zustand, sichtbar über ein Handle)
Die EngineDefinition ist Code. Die mittleren beiden sind geprüfte Daten. Den EngineDescriptor gibt es nur in der CompilerDB; zur Laufzeit liest instantiate seinen Excerpt (§4.4). Nur die EngineInstance lebt — und weil sie lebt, gehört sie der JME und ist über ein Handle sichtbar.
Eine EngineDefinition darf keinen Ring-0-Zustand besitzen: keine Scheduler-Queues, keine Handle-Tabellen, keinen JME-Speicher, keine Filedeskriptoren. Das ist kein Verbot, das jemand durchsetzen müsste — sie hätte gar keinen Ort dafür. Zwischen zwei Aufrufen existiert sie nicht.
5.7 Was über die Phasengrenze nicht geht¶
Meta-Phase Artefakt Laufzeit
---------- -------- --------
describe ---------------> Excerpt als Bytefolge ----> instantiate
Verweise über Excerpt-Hash
MetaRecord als JME-Wert - - - - - - geht nicht - - - - X
DescriptorId - - wird zum Excerpt-Hash, dann abgebildet - -> Laufzeit-Ids
(Arena-Index)
Kein Zustand. Was describe sich gemerkt hat, ist weg. Alles, was instantiate braucht, muss im Deskriptor stehen und die Projektion überleben — oder als Bindung bei der Zulassung dazukommen. Das ist eine nützliche Disziplin: Sie zwingt dazu, die Absicht vollständig auszudrücken.
Keine Werte. Ein MetaRecord ist ein JME-Wert in jenem Lauf. Im Artefakt liegt eine Bytefolge.
Keine Ids. Eine Id ist ein Arena-Index. Im Excerpt steht an ihrer Stelle der Excerpt-Hash des Ziels (§4.4), und der wird beim Laden auf eine Laufzeit-Id abgebildet.
5.8 Welche Engine zuständig ist¶
Für einen Blueprint-Typ gibt es höchstens eine Engine (jdl-blueprint-spec §9.4). Umgekehrt darf eine Engine mehrere Blueprint-Typen bedienen: Im Spec-Platz ihres Protokolls steht dann eine Union dieser Typen, etwa RuntimeEngine[WebSpecs] mit type WebSpecs : union { HttpApplication, StaticSite }. Ein provide pro Blueprint auf demselben Träger ist dafür nicht vorgesehen.
Gesucht wird über Mitgliedschaft. Schreibt der Benutzer HttpApplication & { ... }, sucht die CompilerDB nicht eine Engine, deren Spec genau HttpApplication ist, sondern eine, deren Spec diesen Typ enthält. Weil Unions flach sind und keine Duplikate haben, ist das eine schlichte Mitgliedschaftsprüfung über eine Menge.
Blueprint-Typ B
|
v
alle Engine-Provides durchsehen
|
+-- Spec ist B oder eine Union, die B enthält?
|
+-- genau eine --> zuständige Engine
+-- keine --> Fehler: B hat keine Engine
+-- mehrere --> Fehler: Überschneidung
Trusted Core. Die Suche und die Prüfung auf Überschneidung liegen in der CompilerDB beziehungsweise beim Materializer (§3.5, „Kohärenz“), also in Zig. Die Kohärenzprüfung arbeitet damit über Mengen statt über Einzeltypen: Zwei Engines, deren Unions einen Blueprint gemeinsam haben, sind ein Konflikt, auch wenn keine ihrer Unions der anderen gleicht.
Damit describe die Union wieder auseinandernehmen kann, braucht match Typ-Arme; die sind vorgeschlagen, aber nicht entschieden (jdl-sprachreferenz §16.3). Da Blueprints nur in der Meta-Phase leben, entsteht daraus zur Laufzeit keine Anforderung.
6. Meta-Funktionen¶
6.1 Die Phase steht am Pfeil¶
Die beiden Hälften einer RuntimeEngine sind zwei gewöhnliche Funktionen. Was sie unterscheidet, steht hinter dem Pfeil ihrer Signatur:
describe ->Meta{...} läuft in der Meta-Phase, liefert den Inhalt
ihres Deskriptors als getypten Wert (§5.3)
instantiate ->{...} läuft zur Laufzeit, liest dessen Excerpt,
liefert eine lebende Instanz
Es gibt kein eigenes Schlüsselwort für Compile-Zeit-Code. Ein früheres typefn wurde abgeschafft, und das war mehr als eine Umbenennung.
Mit einem eigenen Schlüsselwort wären describe und instantiate zwei verschiedene Konstrukte gewesen, die zufällig in einem provide-Block nebeneinanderstehen. Mit der Phase am Pfeil sind es zwei gewöhnliche def, die sich nur darin unterscheiden, wann sie laufen.
Und die Zugehörigkeit ist an der Signatur ablesbar. Man muss nicht wissen, welches Protokoll wie heißt — man liest ->Meta{} und weiß Bescheid.
6.2 Meta-Werte sind JME-Werte¶
Meta-Code ist gewöhnlicher JDL-Code. Ein MetaRecord ist ein JME-Objekt wie jedes andere.
Daraus folgt etwas, das beim ersten Lesen überrascht: Beim Übersetzen läuft eine VM. Der erste Meta-Aufruf setzt Generator, JME und VM-Schleife voraus.
Der Compiler ist also kein Werkzeug, das ein Programm verarbeitet. Er ist selbst ein Programm auf derselben Maschine.
6.3 Warum das den Seed nach Zig zwingt¶
Seed in Zig, ohne Meta-Auswertung
|
| durchbricht den Kreis
v
Meta-Code braucht eine laufende VM <-----+
| |
v |
VM braucht Deskriptoren |
| |
v |
Deskriptoren entstehen durch |
Materialisierung |
| |
v |
Materialisierung ist Meta-Arbeit --------+
Der Seed ist der einzige Punkt, an dem noch keine VM existiert, die Meta auswerten könnte. Deshalb entsteht er in Zig, über setInput, ohne JDL-Quelle.
Ein in JDL geschriebener Bootstrap-Kern bräuchte einen Compiler, um zu entstehen — und der bräuchte ihn.
6.4 Die db-Fassade¶
Was eine Meta-Funktion sehen darf, ist eine eingeschränkte Lesesicht auf die CompilerDB:
db.target() db.fields(of:) db.typeOf(field:)
db.name(t:) db.typeParam(t:, index:) db.phantomParam(t:, name:)
db.refinements(of:) db.meta(of:, field:) db.metaRing(field:)
db.hasProvide(p, args) db.variants(of:) db.isMutable(field:)
db.isConcreteType(t:) db.typeKind(t:) db.ring0Fact(t:, field:)
db.isRing0Fact(t:, field:)
Die Einschränkung hat einen technischen Grund, keinen erzieherischen: Jede Operation hier erzeugt eine Abhängigkeitskante. Was an der Fassade vorbei gelesen würde, stünde nicht im Graphen.
Die schreibende Seite ist eine Fassade auf Patches, nicht auf den Speicher — db.setMeta, db.createStruct und ähnliches erzeugen Patcheinträge, keine Wirkung.
Den Kopf eines Deskriptors (§4.9) kann die schreibende Seite nicht setzen. Er gehört dem Materializer.
6.4.1 Semantische Views sind normale Queries¶
Ein Provider soll nicht von der konkreten AST-Form jedes Konstrukts abhängen, wenn er nur eine bestimmte semantische Sicht braucht. Dafür genügt ein bestehender Mechanismus: eine typisierte Derived Query. View ist hier ein Architekturbegriff, kein JDL-Keyword und keine neue CompilerDB-Primitive.
Beispielhaft, als Query-Verträge und nicht als normierte JDL-Syntax:
GetControlFlowView(FunctionSubject) -> ControlFlowView
GetCallView(FunctionSubject) -> CallView
GetEffectView(FunctionSubject) -> EffectView
GetProgressView(LoopSubject) -> ProgressView
Die View ist die kanonische Projektion eines Subjects für eine Analyseachse. Sie ist keine zweite AST-Hierarchie. Ein while, ein for und ein späteres höheres Konstrukt können dieselbe ControlFlowView liefern, ohne dass der Consumer ihre Oberflächensyntax kennen muss.
Das Wort „Projektion" meint hier eine Sicht auf ein Subjekt für eine Analyse. Mit der Projektion eines Deskriptors in einen Excerpt (§4.4) hat es nur das Wort gemeinsam.
Konstrukt / Subject
↓
Provider der View-Query
↓
ControlFlowView / EffectView / ...
↓
mehrere unabhängige Analysen
Damit funktioniert Erweiterung in beide Richtungen:
neues Konstrukt
-> beteiligt sich an der JDL-seitigen View-Semantik
-> der eine View-QueryProvider kann es projizieren
-> bestehende Analysen bleiben brauchbar
neue Analyse
-> konsumiert bestehende Views
-> vorhandene Konstrukte müssen nicht um Analyse-Methoden wachsen
Das ist wichtig wegen der CompilerDB-Eindeutigkeit: Ein neues Konstrukt registriert nicht einfach einen zweiten GetControlFlowView-QueryProvider. Der pro Stage-Profil eindeutige View-Provider delegiert intern über normale JDL-Semantik, etwa provide/Dispatch oder einen anderen eindeutig geregelten Mechanismus, an die konstrukt-spezifische Projektion. Die genaue Ring-1-Schnittstelle dafür ist noch zu normieren.
Ring 0 soll dafür nur generische Query-/Provider-Mechanik tragen. Die aktuelle CompilerDB-Spec erreicht dieses Ziel für neue Query-Arten allerdings noch nicht vollständig: QueryKey.tag ist dort hostseitig typisiert und neue Queries werden als Host-struct definiert. Bestehende Query-Verträge können bereits durch JDL-EngineProvider erfüllt werden; damit JDL auch völlig neue View-Verträge definieren kann, braucht die CompilerDB zusätzlich eine generische, datengetriebene Query-Vertragsregistrierung. Das ist eine CompilerDB-Lücke, kein Grund für eine View-spezifische Registry.
Ein View-Ergebnis ist zunächst ein gewöhnliches QueryResult; es wird nur dann zum Deskriptor, wenn es die allgemeinen Deskriptor-Kriterien tatsächlich erfüllt und materialisiert werden muss.
Views sollten außerdem nicht atomar klein geschnitten werden. GetControlFlowEdge17 wäre zwar herrlich konsequent und praktisch unbrauchbar. Eine View ist die kleinste sinnvoll wiederverwendbare semantische Projektion, damit Query-Dispatch, Serialisierung und Dependency-Buchhaltung nicht die Analyse dominieren.
6.4.2 Facts, Obligations und Proofs¶
Eine Analyse kann auf Views einen Beweis aufbauen, ohne dass das Konstrukt den Beweis selbst kennt. Drei Begriffe reichen:
Proof-Fact
semantisch bekannte Aussage aus einer View oder Query; kein neuer CompilerDB-Begriff
Obligation
Aussage, die eine Proof-Regel zusätzlich benötigt
Proof
Ableitung aus Proof-Fakten und erfüllten Obligations
Beispiel Totalität:
Total(foo)
-> ControlFlowView(foo)
-> Terminates(loop42)
-> ProgressView(loop42)
-> Finite(range)
-> Total(bar)
-> proven
Die Proof-Regel besitzt die domänenspezifische Bedeutung. Die CompilerDB besitzt sie nicht; sie sieht nur die Queries und deren Reads. Für einen Proof-Query-Vertrag gilt weiterhin die normale Provider-Regel: pro Stage-Profil genau ein aktiver QueryProvider. Verschiedene Label- oder Proof-Regeln sind deshalb keine konkurrierenden CompilerDB-Provider derselben Query. Sie werden innerhalb der JDL-Semantik des zuständigen Providers beziehungsweise über eindeutig getrennte Unterqueries komponiert.
Das Judgement eines Proof-Queries ist domänenspezifisch. Die Label-Semantik verwendet beispielsweise proven, excluded und unproven; ein Totalitätsnachweis muss daraus nicht künstlich dieselben drei Zustände übernehmen. Er kann etwa nur „Totalität bewiesen“ gegen „nicht bewiesen“ unterscheiden, ohne damit Nichtterminierung zu behaupten.
Für alle konservativen Proofs gilt jedoch dieselbe logische Grenze: fehlender Beweis ist kein Gegenbeweis. Gerade bei Totalität ist das unvermeidbar: Für eine hinreichend ausdrucksstarke Sprache existiert kein vollständiger allgemeiner Terminierungsentscheider. Jade braucht daher konservative Proof-Regeln, etwa für endliche Iteration, strukturelle Rekursion oder ein nachweislich fallendes Maß. Was außerhalb dieser Regeln liegt, darf unbewiesen bleiben.
6.4.3 Closure ohne Query-Zyklus¶
Eine Fachanalyse kann einen lokalen Fixpunkt brauchen; die Label-Closure ist das konkrete Beispiel. Das ist aber kein CompilerDB-Fixpunkt über zyklische Queries. Query-Zyklen bleiben ein Fehler, weil die Datenbank sonst keine eindeutige Antwort auf eine Frage besitzt.
Wo der jeweilige Fachvertrag einen solchen Fixpunkt vorsieht, läuft er lokal innerhalb einer terminierenden Provider-Auswertung:
Provider erhält endliche Ausgangsfakten
↓
Worklist
↓
Regeln anwenden
↓
neue Fakten monoton hinzufügen
↓
keine Änderung mehr
↓
QueryResult publizieren
Reads während dieser Berechnung laufen weiterhin über db.*. Die CompilerDB sieht damit alle externen Abhängigkeiten; der temporäre Worklist-Zustand bleibt lokaler Meta-Arbeitszustand und gehört nicht in den DepGraph. Das ist dieselbe Trennung wie bei jedem anderen komplexen Provider: CompilerDB speichert Wissen, nicht jeden Zwischenschritt seiner Berechnung.
Bei rekursiven Call-/Typgraphen muss der Provider seine endliche Arbeitsdomäne explizit beherrschen, etwa über SCCs, visited sets oder eine finite Faktengitterhöhe. Er darf Rekursion nicht dadurch „lösen“, dass er denselben QueryKey erneut an die CompilerDB stellt.
Das View-/Proof-Modell schreibt nicht allen Analysen denselben Solver vor. Die Label-Semantik besitzt einen monotonen Least-Fixpoint-Vertrag; Totalität ist ein konservativer regelbasierter Nachweis; andere Analysen können andere endliche, deterministische Verfahren benötigen. Eine lokale Closure ist damit ein zulässiges Werkzeug, nicht die Definition von „Proof“.
6.4.4 Kostenmodell¶
Dass diese Semantik in JDL läuft, ist nicht automatisch der teure Teil. Teuer wird sie, wenn sie dieselben Fakten wiederholt oder global vorsorglich berechnet. Deshalb gelten für semantische Views und Proofs dieselben Regeln wie für den Rest der CompilerDB:
- demand-driven statt Whole-Program-Vorlauf;
- QueryResults cachen und durch Reads invalidieren;
- Early Cutoff nutzen;
- Views ausreichend grob schneiden;
- Closures nur über den tatsächlich betroffenen endlichen Graphen beziehungsweise eine SCC laufen lassen;
- parametrische Beweise möglichst als wiederverwendbare Regeln erhalten statt jede Generic-Instanz blind neu zu entdecken;
- Provenance getrennt von semantischen Ergebnissen halten und revisionsgebunden kompaktierbar lassen.
Der performancekritische Substratteil bleibt damit in Zig: Cache, DepGraph, Query-Dispatch, Revisionen, Cancellation und Speicherverwaltung. JDL formuliert die semantische Arbeit darauf.
6.5 Die Meta-Phase endet nicht¶
Ein häufiges Missverständnis: Die Meta-Phase sei ein Vorlauf, der aufhört, wenn die Laufzeit beginnt.
falsches Bild REPL oder Sprachserver
Compile ---> Runtime Meta-Context läuft durchgehend
Laufzeit-Context läuft daneben
Bei einem einmaligen Übersetzen sieht es wie ein Vorlauf aus. Bei einem Sprachserver gibt es kein Ende der Übersetzung, und bei einer REPL existieren beide Umgebungen gleichzeitig und dauerhaft.
Es gibt keinen Umschaltpunkt. Es gibt zwei Ausführungsdomänen mit verschiedenen Autoritäten.
Das heißt nicht, dass eine laufende Anwendung beschreiben darf. Ein Laufzeit-Context ruft kein describe auf. Neue Contexts kommen zur Laufzeit nur als fertige Artefakte hinzu, die zugelassen und aktiviert werden (wie-jade-ausfuehrt §5.5).
6.6 Rows in der CompilerDB¶
Seit 2026-10-05 ist eine Row ein Typ (jdl-effect-rows-und-throw §3.6). Für die CompilerDB heißt das vor allem: Rows sind kein Sonderfall mehr. Sie werden wie andere Typen beschrieben, gehasht, abgefragt und invalidiert.
Jede Row hat eine Phase. Eine Row ist entweder eine Laufzeit-Row (row::Row) oder eine Meta-Row (row::MetaRow); eine dritte Möglichkeit gibt es nicht (jdl-effect-rows-und-throw §3.7). Die Phase ist ein Bit im Deskriptor, gesetzt aus dem Seed, nicht vom Benutzer überschreibbar.
Eine Row hat einen Deskriptor. Ihr Inhalt ist die normalisierte Menge ihrer Elemente: ohne Duplikate, Unions über Throw aufgelöst, in fester Ordnung (jdl-effect-rows-und-throw §3.2). Daraus und aus der Phase entsteht ihr contentHash. Die leere Laufzeit-Row und die leere Meta-Row sind deshalb zwei verschiedene Einträge.
Ein Row-Name geht nicht in den Hash ein. Row-Namen sind durchsichtig: ShopNeeds ist eine Abkürzung für {Orders, Payments}, kein eigener Typ. Damit weicht eine Row bewusst von der Regel für benannte Deskriptoren ab (§4.3). Zwei Rows mit gleicher Phase und gleichem Inhalt sind derselbe Eintrag, gleichgültig, wie sie heißen; der Name ist nur ein Weg zum Nachschlagen.
Die feste Ordnung ist zweimal wichtig. Beim Übersetzen sorgt sie für einen stabilen Hash. Zur Laufzeit bestimmt dieselbe Ordnung die Plätze des Bündels, mit dem eine gelieferte Row an Aufrufe weitergereicht wird (wie-jade-ausfuehrt §4.5). Compiler und VM müssen sie deshalb nach exakt derselben Regel berechnen.
Die Summe über einen Aufrufgraphen ist eine Query. Was alle Funktionen unter einem Einstiegspunkt zusammen brauchen, schreibt niemand mehr ab; der frühere requires-Block entfällt. Ein Context oder CallGraph fragt die Summe ab und vergleicht sie mit seinem Angebot.
Summe(Einstiegspunkt)
|
+--> Signatur des Einstiegspunkts lesen (Kante)
+--> Aufrufe aus dem Rumpf lesen (Kante, über eine View)
+--> für jedes Ziel: dessen Signatur lesen (Kante)
|
v
Vereinigung der deklarierten Rows
Weil jede Funktion ihre Row deklariert, ist das ein einfacher Durchlauf und kein Fixpunkt: Rekursion zwischen f und g bedeutet nur, dass beide Signaturen gelesen werden. Der Durchlauf liest Signaturen, keine Rümpfe von Zielen, und jede Lesung erzeugt eine Kante. Ändert sich eine Signatur tief unten, wird die Summe neu berechnet; ändert sich nur ein Rumpf, greift Early Cutoff (§9.2).
Rows sind über den Query-Record abfragbar. „Welche Row hat diese Funktion?“, „Was ist der Inhalt dieses Row-Platzes für diese Implementierung?“ und „Was braucht dieser Graph?“ sind Kandidaten für Fragen im Query-Record (§3.7.2). Welche davon Zig anbietet, gehört zur offenen Frage nach dessen Umfang (§11).
Trusted Core. row steht in der fest verdrahteten TypeKind-Liste. Die Normalisierungsregel, aus der Hash und Bündelordnung folgen, ist Teil des Kerns, nicht der Stdlib. Dasselbe gilt für die Phase: Der Compiler vergleicht die Phase am Pfeil von sich aus mit der Phase einer benannten Row, und in einen Deskriptor, den die VM lädt, gelangt nur eine Laufzeit-Row (jdl-effect-rows-und-throw G-11). Die Rollen Row und MetaRow stehen deshalb im Seed, nicht in Nephrit.
Was über die Grenze reist. Allgemeiner gilt: Aus der Meta-Phase gelangen nur Records in den Excerpt, und ein Record hält nur Daten, auch keinen aufrufbaren Funktionswert, sondern höchstens den Hash einer Funktion (00-ontology-types-tags-phantoms §3a.3). Das ist die Eigenschaft, die den Materializer und den Lader Records ohne JDL-Code verarbeiten lässt.
7. Zwei Engines im Überblick¶
Die folgenden Beschreibungen zeigen an zwei Engines, wie die Regeln aus §3 bis §6 zusammenspielen. Sie beschreiben Verhalten, keine Syntax.
7.1 Eine Engine, die nur beschreibt¶
Eine Codec-Engine erzeugt für einen Typ die Regeln, nach denen seine Werte kodiert werden. Ihr Blueprint nennt den Zieltyp, das Wire-Format und eine Feldregel.
Sie beginnt mit einer Query. Zuerst fragt sie über die db-Fassade, welche Art Typ das Ziel ist. Damit entsteht die erste Abhängigkeitskante, ohne dass jemand sie deklariert. Ändert sich die Art des Typs, läuft die Engine neu.
Sie behandelt nur, was sie kennt. Für Structs und Enums baut sie einen Codec. Für jede andere Art wirft sie eine Diagnose und baut nichts.
Die Rekursion läuft über die Datenbank. Für jedes Feld braucht sie den Codec des Feldtyps. Den berechnet sie nicht selbst, sondern fragt die CompilerDB danach — mit allen drei Vorteilen aus §5.4.
Diagnosen werden gesammelt, nicht sofort geworfen. Fehlt für mehrere Felder ein Codec, sieht der Nutzer alle auf einmal statt einen nach dem anderen.
Geworfen wird vor dem Patch. Solange etwas fehlschlagen kann, baut die Engine nichts. Bis dahin hat sie ausschließlich gelesen.
Am Ende liefert sie ein PatchSet: ein neues Provide für den Codec und einen Meta-Eintrag mit dem Wire-Format. Gültig wird es erst, wenn der Materializer es committet.
7.2 Eine Engine mit beiden Hälften¶
Eine HTTP-Engine hat beide Hälften.
describe arbeitet wie in §7.1. Es prüft die Routen, löst die Codecs auf und baut die Middleware-Kette, alles über die db-Fassade. Heraus kommt der Inhalt des Server-Deskriptors mit fertiger Routentabelle; die CompilerDB trägt ihn als Patch ein (§5.3).
instantiate ist gewöhnlicher Laufzeitcode. Seine Row nennt die Services, die es braucht, etwa Netz und Storage, und die Fehler, die es werfen kann. Die Services bindet der Context; die Fehler muss der Aufrufer behandeln (wie-jade-ausfuehrt §4.3).
Es liest, statt zu rechnen. Die Routentabelle steht im Excerpt des Deskriptors. instantiate übernimmt sie und leitet nichts ab.
Es bekommt seine Umgebung, statt sie zu wählen. Der Socket, auf dem der Server lauscht, wird bei der Zulassung als Resource gebunden (§4.4). Die Engine sieht ihn nur über einen Service in ihrer Row, den der Context bei der Aktivierung an diese Resource bindet; welcher Port das ist, steht nicht im Artefakt.
Mehr als seine Row bekommt es nicht. Braucht es etwa eine benannte Storage-Region, steht Storage in seiner Row. Einen Zugang zum Context daneben gibt es nicht (wie-jade-ausfuehrt §5.2).
Heraus kommt eine EngineInstance. Sie lebt, hat Zustand und ist über einen Handle sichtbar (§5.6).
8. Der ganze Durchlauf¶
Quelltext
|
v
Query: GetTypeDescriptor
|
v
im Cache?
|
+-- ja ---> QueryResult <---------------- Cache ---------------------+
| |
+-- nein -> Engine läuft als Provider |
| |
| db.* |
v |
liest, erzeugt Kanten ---------------------+ |
| | |
v | |
Provider erfolgreich? | |
| | |
+-- nein --> Failed(Provider) ------------+-------------+
| | |
+-- ja ----> PatchSet --------------------+ |
| | |
v | |
Materializer prüft | |
und schreibt den Kopf | |
| | |
+-- abgelehnt --> Failed(Materializer) ---+
| | |
+-- ok --> Interning über | |
contentHash | |
| | |
v | |
DescriptorStore -+-------------+
semanticPayload |
| v
| DerivationRecord
| reads, patchSet,
| outcome
| |
| v
| ProvenanceStore
v
Selektion: von den Wurzeln erreichbar
|
v
Projektion: je Deskriptor ein Excerpt
|
v
Artefakt
|
v
Laden und Zulassen
|
v
instantiate
|
v
JME, Scheduler, RuntimeBus
9. Was bei einer Änderung passiert¶
Abschnitt 3.3 sagt, dass die Abhängigkeitskanten durchs Lesen entstehen. Hier steht, was daraus folgt — und warum ein Sprachserver nicht extra gebaut werden muss.
9.1 Revisionen statt Neuübersetzung¶
Jedes setInput — eine geänderte Datei, ein neuer Build-Schalter — zählt eine Revisionsnummer hoch. Es passiert sonst nichts. Keine Phase läuft an, nichts wird verworfen.
Erst wenn jemand wieder etwas fragt, wird geprüft, ob die Antwort noch gilt. Das ist der Unterschied zwischen einem Cache, der aktiv geräumt wird, und einem, der faul nachprüft.
9.2 Early Cutoff — warum die Kettenreaktion meistens ausbleibt¶
Der wichtigste Mechanismus, und er wird leicht unterschätzt.
Datei geändert: ein Kommentar
|
v
GetFileAST läuft neu
|
v
Ergebnis gleich wie vorher?
|
+-- ja ---> Ungültigkeit propagiert NICHT weiter
|
+-- nein -> Abhängige neu prüfen
|
v
deren Ergebnis gleich?
|
+-- ja ---> hier ebenfalls Schluss
|
+-- nein -> weiter nach oben
Ein Eintrag trägt zwei Zahlen: wann er zuletzt geändert wurde und wann er zuletzt überprüft wurde. Liefert eine Neuberechnung dasselbe Ergebnis, wird nur die Prüfzahl aktualisiert, nicht die Änderungszahl — und alles, was darauf aufbaut, bleibt gültig.
Eine Kommentaränderung lässt das AST neu entstehen und endet dort. Eine Umbenennung einer privaten Hilfsfunktion endet nach zwei Ebenen. Nur eine Signaturänderung reist wirklich nach oben.
9.3 Durability — nicht alles ändert sich gleich oft¶
Inputs tragen eine Einstufung, wie wahrscheinlich sie sich ändern. Der Seed ist das Extrem: Er ändert sich während eines Laufs nie.
Damit muss eine Kante auf Seed-Wissen nie geprüft werden. Sie feuert nicht, weil sie nicht feuern kann. Das ist der Grund, warum die grobe Auflösung beim Seed — ein Input für alles — nichts kostet.
9.4 Cancellation¶
Ein Sprachserver erzeugt Revisionen schneller, als Berechnungen fertig werden. Eine laufende Auswertung, die durch einen Tastendruck überholt wird, muss abbrechen können.
Prüfpunkte liegen an definierten Stellen: vor einem Provider-Aufruf, zwischen Engine-Phasen. Eine abgebrochene Berechnung lässt den vorhandenen Cache unangetastet — sie hat ja nur gelesen.
9.5 Wohin das Alte geht¶
Ein Cache-Eintrag wird bei Neuberechnung ersetzt. Eine Arena kann einzelne Objekte aber nicht zurückholen.
Der Entwurf dazu: Jeder Eintrag hält zwei Plätze fester Größe. Eine Neuberechnung schreibt in den gerade ungültigen und wird damit zum gültigen. Kein Allozieren, kein Kopieren, kein Verschnitt.
vorher nachher
+--------------------+ +--------------------+
| Platz A: gültig | | Platz A: alt |
+--------------------+ -----> +--------------------+
| Platz B: alt | | Platz B: gültig |
+--------------------+ +--------------------+
^
|
Neuberechnung
schreibt hierhin
Passt ein Ergebnis nicht in den Platz, weicht es in die Arena aus und der Eintrag zeigt dorthin. Der Verschnitt bleibt auf diese Ausreißer begrenzt.
Das funktioniert nur, weil Query-Ergebnisse keine fremden Zeiger enthalten dürfen und kanonisch serialisierbar sind. Dieselbe Regel, die als Lebensdauerschutz gedacht war, macht das Umschreiben überhaupt erst möglich.
9.6 Zwei Annahmen, die dazugehören¶
Der Compiler läuft einthreadig — vorerst, und als benannte Annahme, nicht als stillschweigende. Sie gilt pro Compilerkontext, nicht pro Prozess: Ein eingebauter Sprachserver öffnet einen zweiten Kontext, und der hat seinen eigenen Strang.
Eine gelesene Bytefolge gilt bis zum nächsten Prüfpunkt. Wer sie länger braucht, kopiert. Das ist der Satz, der verhindert, dass ein Leser zwei Werte aus verschiedenen Revisionen mischt und etwas erhält, das nie gegolten hat — ein Typ mit dem Layout von vorhin und den Labels von jetzt.
Die Konsistenz hängt damit an der Prüfpunktdichte, nicht an der Anzahl der Plätze. Das ist die günstigere Stellschraube: Prüfpunkte kann man nachträglich einfügen, ein drittes Fach nicht.
10. Was man dadurch geschenkt bekommt¶
Die folgenden Eigenschaften hat niemand eingebaut. Sie fallen aus der Bauart.
Inkrementelles Übersetzen. Eine Engine, die als Provider läuft und ihre Kanten durchs Lesen erzeugt, ist automatisch invalidierbar. Der Sprachserver braucht keine Sonderbehandlung.
Die Information für einen DerivationRecord entsteht ohnehin — aber an zwei Stellen:
reads während der Provider-Auswertung
patchSet nur bei erfolgreicher Provider-Auswertung
outcome beim terminierenden Schritt — Provider oder Materializer
Alles davon fällt als Nebenprodukt an, nichts wird extra erhoben. :why rekonstruiert nichts, es liest ab.
Was das an Speicher und Laufzeit tatsächlich kostet, ist nicht gemessen — und seit die Provenance mehrwertig ist (§4.5), wächst sie pro Ableitungsweg, nicht pro Deskriptor. Das ist die Stelle, an der aus „entsteht ohnehin" irgendwann ein Sprachserver mit drei Gigabyte werden kann.
Ein Fehlschlag hinterlässt keinen halben Zustand. Weil eine Engine nur liest und ein PatchSet abliefert, entsteht kein partiell materialisierter semantischer Zustand. Dieselbe Eigenschaft wie beim Plugin-Staging, nur eine Ebene tiefer.
Folgenlos ist er deshalb nicht: Er hinterlässt absichtlich ein QueryResult::Failed und einen DerivationRecord (§4.5). Was folgenlos bleibt, ist der semantische Commit.
Deduplizierung passiert von selbst. Unveränderliche Einträge mit contentHash internen sich. Option[i32] und Result[i32, E] teilen sich womöglich denselben LayoutDescriptor, ohne dass es jemand plant.
Ein Artefakt ist über Umgebungen hinweg identisch. Weil umgebungsabhängige Werte erst bei der Zulassung dazukommen (§4.4), läuft auf dem Laptop und in Produktion nachweislich derselbe Code — derselbe Hash.
Ein Parser in JDL ist nichts Besonderes. Er ist der Provider für GetFileAST und wird gecacht und invalidiert wie alles andere — inkrementelles Parsen fällt dabei ab.
Ob man ihn eine „ParserEngine" nennt, hängt daran, wie eng man Engine definiert. Nach §5.3 liefert eine Engine ein PatchSet; ein Parser liefert ein AST als Query-Ergebnis und materialisiert nichts. Er ist damit Provider, aber keine DescriptorEngine — dasselbe Verhältnis wie in §5.1: Jede Engine kann Provider sein, nicht jeder Provider ist eine Engine.
Und Plugins sind kein Sonderfall. Ein Plugin bringt Provider mit. Der Rest der Maschinerie merkt keinen Unterschied.
Der Bedarf eines Programms ist abfragbar. Seit Rows Typen sind, ist die Summe über einen Aufrufgraphen eine gewöhnliche Query (§6.6). Ein Werkzeug kann für jeden Einstiegspunkt anzeigen, welche Services er insgesamt braucht, ohne dass jemand eine Liste pflegt.
10.1 Was Semantic Views zusätzlich bringen¶
Der gleiche Query-Mechanismus entkoppelt jetzt nicht nur Compilerphasen, sondern auch Sprachkonstrukte von Analysen. Ein neues Konstrukt muss nicht provePure, proveTotal und zwanzig spätere Methoden mitbringen. Es muss die semantischen Projektionen liefern, die für seine Bedeutung gelten. Umgekehrt muss eine neue Analyse nicht alle AST-Kinds kennen, wenn die benötigten Views bereits existieren.
Das ist die praktische Self-Hosting-Grenze: Ring 0 weiß, wie eine Query ausgeführt wird. JDL weiß, was ControlFlow, Progress, Pure oder Total bedeuten.
11. Was offen ist¶
Ehrlichkeitshalber, damit dieses Dokument nicht mehr verspricht, als dasteht.
JDL-definierbare Query-Verträge. Die aktuelle CompilerDB kann JDL-Provider für vorhandene Queries ausführen, modelliert neue Query-Arten selbst aber noch hostseitig über TypeTag/Query-Structs. Für echtes Self-Hosting muss dieser Vertrag datengetrieben registrierbar werden. Die konkrete Identitäts- und Schemaform ist noch offen; eine View-spezifische Sonderregistry ist ausdrücklich nicht die Lösung. Die Arbeitsrichtung dafür steht in §3.7: Fragen als Records, Schemas als Daten.
Umfang des Query-Records (§3.7.2). Offen ist, welche Fragen und Verknüpfungen Zig anbietet. Jede weitere ist Trusted-Core-Code. Offen ist auch die Form: Varianten pro Begriff — Felder, Varianten, Provides — oder allgemeine Operationen wie „auswählen nach Rolle“, „filtern nach Feld“, „verknüpfen über Verweis“. Allgemeine Operationen lassen den Record seltener wachsen, weil er nur bei neuen Operationen wächst, nicht bei neuen Begriffen; die begriffsnahen Methoden blieben dann im Builder. Naheliegender Start: nur das, was die ersten Engines brauchen, und nachlegen, sobald eine Engine sonst eine N+1-Schleife schreiben müsste. Seit 2026-10-05 kommen Fragen nach Rows hinzu (§6.6). Ebenfalls offen: ob der Query-Record ein enum mit Record-Nutzlast oder eine union über eigene Frage-Typen wird (§3.7.2).
Rückgabetyp von db.ask (§3.7.3). Der Typ der Antwort hängt an der Art der Frage. Ob das über eigene Frage-Typen im Builder läuft oder das Typsystem es an einem Union ausdrücken kann, ist offen.
Schema als Daten (§3.7.4). Offen sind das Format der Schema-Beschreibung, wie Suchwege angemeldet werden und wer sie prüft. Trusted Core: Zig liest Inhalte anhand des Schemas; ein fehlerhaftes Schema darf nie zu falschem Lesen führen. Naheliegend ist, dass der Materializer ein Schema beim Commit prüft.
Feste und revisionsabhängige Fakten (§3.7.5). Zig muss bei jeder Operation wissen, ob sie auf unveränderlichen Inhalt zugreift oder auf das, was in der aktuellen Revision gilt. Wie das im Schema oder im Query-Record ausgedrückt wird, ist offen. Trusted Core: Wird die Unterscheidung verfehlt, liefert eine Frage Antworten aus einer Revision, die nicht mehr gilt.
Kernmenge semantischer Views. Das Modell legt fest, was eine View ist, aber noch nicht, welche Query-Verträge zum stabilen Ring-1-Kern gehören. ControlFlow, Calls, Effects, Structure und Progress sind Kandidaten, keine hier normierten API-Namen.
View-Dispatch. Weil CompilerDB pro Query und Stage-Profil nur einen Provider zulässt, braucht Ring 1 einen eindeutigen Mechanismus, mit dem neue Konstrukte ihre Projektion für einen bestehenden View-Vertrag beitragen können. Normales JDL-provide/Dispatch ist der naheliegende Kandidat; die konkrete Protokollform ist noch nicht festgelegt.
Proof-Autorität. Für sealed/core Labels muss feststehen, welche JDL-Definitionen beziehungsweise Provider berechtigt sind, einen positiven oder negativen Beweis zu publizieren. Ein Plugin darf nicht allein durch Registrierung Core-Sicherheitsgarantien neu definieren.
Namespace/API-Schnitt. Die konkrete privilegierte Query-/Provider-Oberfläche muss der bestehenden Namespace-Regel folgen: primitive Systemoberflächen liegen in jade::, user-facing Fassaden in jdl::. Für Semantic Views und Proof-Queries werden hier bewusst noch keine öffentlichen API-Namen festgelegt.
Parametrische Proofs. Generische Regeln wie Copyable(T) -> Copyable(Box[T]) sollten nicht bei jeder Instanz als vollständige Herleitung neu entdeckt werden. Form und Cache-Identität solcher Proof-Schemata sind noch nicht festgelegt.
Zyklische Verweise. Verweise laufen über den Hash ihres Ziels (§4.5), im Excerpt über den Excerpt-Hash (§4.4). Zeigt ein Deskriptor über Umwege auf sich selbst — etwa ein Typ mit einem Feld seines eigenen Typs —, braucht sein Hash den eigenen Hash. Dann gibt es auch kein „von unten nach oben“, nach dem die Projektion rechnen könnte. Wie ein solcher Zyklus gehasht wird, ist offen. Trusted Core: Die Zulassung muss dieselbe Regel anwenden wie der Compiler, sonst passen die Hashes nie.
Schema-Versionen. schema steht im Kopf (§4.9). Offen ist, wann ein Empfänger ein anderes Schema lesen darf als das, für das er gebaut wurde — ob nur bei exakter Gleichheit oder nach einer festgelegten Verträglichkeitsregel. Trusted Core: Ring 0 trifft diese Entscheidung bei der Zulassung.
Name und Excerpt-Hash. Bei benannten Deskriptoren ist der Name Teil des Inhalts (§4.3). Nicht exportierte Namen dürfen im Excerpt fehlen (§4.4). Offen ist, ob das Weglassen den Excerpt-Hash ändert oder ob der Name über einen eigenen Weg in den Hash eingeht. Die Frage hat eine handfeste Folge: Geht der Name nicht ein, bekommen zwei benannte Typen gleicher Struktur — Meter und Second — im Artefakt denselben Excerpt-Hash und fallen dort zusammen.
Signaturprüfung zwischen Export und Rumpf. Der Rumpf zeigt auf seinen Export (§4.9), und Ring 0 prüft beim Laden, dass beide dieselbe Signatur haben. Vorgeschlagen ist ein gemeinsamer Signatur-Excerpt, auf den beide verweisen; offen ist, ob es dabei bleibt. Trusted Core: Fehlt die Prüfung, kann ein Artefakt eine Signatur versprechen und Code mit einer anderen dahinterhängen.
Deskriptorliste der CompilerDB-Spec. jade-compiler-db-spec §3.7 zählt Einträge zu den Deskriptoren, die das Kriterium in §4.1 ausschließt. Die Liste muss nachziehen.
Version gegen Revision. Die CompilerDB-Spec sagt, Änderungen erzeugten „neue Descriptor-Versionen oder neue Revisionen". An anderer Stelle spricht sie von Revisionen des ganzen Bestands, dann wieder von Versionen einzelner Einträge. Zwei Mechanismen, und wann welcher greift, steht nicht da.
Optionale Felder und kanonische Form. Für einen stabilen contentHash muss feststehen, ob ein fehlendes Feld dasselbe ist wie ein leeres.
Der Katalog beantwortet nur Ja-Nein-Fragen. db.hasProvide liefert bool. Eine Engine, die auf einer anderen aufbaut, braucht aber eine Referenz auf deren Ergebnis — die Codec-Engine in §7.1 etwa auf den Codec eines Feldtyps. Eine solche Query führt der Katalog noch nicht.
Der ProvenanceStore. §4.5 trennt ihn ab und gibt ihm mit DerivationRecord eine Form, aber sein Zuschnitt ist offen: welche Ableitungen aufbewahrt werden, wie lange, und ob abgelehnte anders behandelt werden als erfolgreiche. Ein Sprachserver erzeugt Ableitungen in derselben Frequenz, in der er Revisionen erzeugt — und abgelehnte entstehen beim Tippen häufiger als erfolgreiche.
Speicher bei lang laufenden Meta-Contexts. Ein Sprachserver oder eine REPL läuft tagelang. Die CompilerDB ist sparsam mit Arbeit, aber nicht von selbst mit Speicher: DepGraph und vor allem Provenance wachsen mit jeder Ableitung, und der DescriptorStore wächst nur. Arbeitsrichtung:
Envelope setzt das Speicherbudget des Meta-Contexts
|
v
Ring 0 meldet dem Context: Budget knapp (vor der Grenze, nicht an ihr)
|
v
1. Provenance verwerfen kostet nur Erklärungen für :why
|
v
2. Cache verdrängen selten Gefragtes zuerst; kostet Neuberechnung
|
v
3. Context zurücksetzen verwirft die ganze CompilerDB-Instanz,
am besten, wenn der Editor still ist;
Warmstart aus einem persistenten Cache
Der Context entscheidet, was entbehrlich ist; Ring 0 führt es aus. Der Reset braucht keinen eigenen Mechanismus: Gehört die CompilerDB-Instanz genau einem Context, verschwindet sie mit ihm. Nach außen gelangen nur Ids, die ohnehin lauf-lokal sind; Hashes und Namen überleben. Der Warmstart ist möglich, weil der contentHash über Läufe stabil ist (§4.3).
Trusted Core: Das Signal „Budget knapp“ ist ein Weg von Ring 0 zum Context, der noch nicht beschrieben ist. Und das Aufräumen darf nicht nur an Revisionen hängen — ein Modus, der Arbeit erzeugt, ohne die Revision zu erhöhen, würde sonst nie aufräumen. Vergleichbare Systeme sind genau daran gescheitert.
Die Deskriptor-Taxonomie. Es existieren rund sechsundsechzig verschiedene Namen auf …Descriptor, davon achtunddreißig in genau einem Dokument. Die Kernmenge ist nicht abgegrenzt.
Das Speichermodell. Sobald zwei Tasks denselben Wert sehen, muss feststehen, was der Generator umordnen darf. Das ist keine Compiler-Frage, aber es betrifft alles, was hier gebaut wird.¶
Die andere Hälfte — RuntimeBus, Context, Scheduler, JME und was
instantiatetut — steht inwie-jade-ausfuehrt.
Änderungsprotokoll¶
2026-10-05
- §3.7.2: Widerspruch benannt — ein
union-Block nennt seit 2026-10-05 bestehende Typen; die Query-Skizze mit benannten Varianten wäre einenumoder eineunionüber eigene Frage-Typen. §11 entsprechend ergänzt. - §5.5:
instantiatehat einen Row-Platz statt einer Row-Variable. - §5.8 neu: Welche Engine zuständig ist — Union im Spec-Platz, Suche über Mitgliedschaft, Kohärenz über Mengen (Trusted Core).
- §6.6 neu: Rows in der CompilerDB — Deskriptor und Hash über den normalisierten Inhalt, Row-Namen gehen nicht in den Hash ein, feste Ordnung für Hash und Laufzeit-Bündel, Summe über einen Aufrufgraphen als Query statt
requires, Rows als Kandidaten für den Query-Record; Trusted Core:rowin der TypeKind-Liste, Normalisierungsregel im Kern. - §10: Der Bedarf eines Programms ist abfragbar.
- §11: Umfang des Query-Records um Row-Fragen und die Form
enumgegenunionergänzt. - §6.6, zweiter Durchgang: Jede Row hat eine Phase (Laufzeit oder Meta), die Phase geht in den Hash ein; Trusted Core: Phase am Pfeil gegen Phase der Row, nur Laufzeit-Rows in geladenen Deskriptoren (G-11), Rollen im Seed. Neu: Was über die Grenze reist — nur Records, ohne aufrufbare Funktionswerte.
2026-10-02
- §4.4: Verweise zeigen im Excerpt auf den Excerpt-Hash ihres Ziels. Die Projektion stellt sie um und rechnet dafür von unten nach oben; die Zulassung rechnet mit derselben Regel nach (Trusted Core). §4.2-Diagramm, §4.5, §4.7, §5.7 und §4.9 entsprechend angepasst.
- §4.9: Der Schnittstellen-Hash ist der Excerpt-Hash des Exports: Name und Signatur samt Row, ohne Rumpf. §4.4: Der Rumpf eines Exports gehört zur Selektion.
- §4.9: Der Rumpf zeigt auf seinen Export, nicht umgekehrt; die Zuordnung ist gewöhnlicher, gehashter Inhalt. Trusted Core: Verzeichnis „Export → Rumpf“ beim Laden, genau ein Rumpf je Export, gleiche Signatur. §11: Statt „Vom Export zum Rumpf“ bleibt nur offen, wie die Signaturprüfung genau aussieht.
- §4.5: „Heißer Kopf“ heißt jetzt „Heiße Felder, kalter Rumpf“. Der Kopf besteht nur aus den vier Feldern aus §4.9;
size,alignund Ähnliches sind Inhalt, der nur im Speicher daneben liegt. - §3.7.3: Builder-Typen eingeordnet; die Verknüpfung „Felder mit Provide“ fehlt in der Skizze des Query-Records und ist als offen benannt.
- §5.5, §5.6:
instantiateliest den Excerpt, nicht den Deskriptor. - §6.1:
describeeiner RuntimeEngine liefert den Inhalt ihres Deskriptors, kein PatchSet (wie §5.3). - §7.2: Die Engine sieht den Socket über einen Service in ihrer Row, nicht als eigenen Handle.
- §11: „Zyklische Verweise“ und „Name und Excerpt-Hash“ um die Folgen des Excerpt-Hashes ergänzt. Die zwei Punkte zu Version und Revision zusammengelegt; der zweite verwies auf falsche Abschnitte.
- §5.3: Wie das Liefern eines getypten Deskriptors bei RuntimeEngines zur Regel „Patch, kein Deskriptor“ passt.
- §7.2: Kein Zugang über die ContextApi mehr;
instantiatebekommt nur, was seine Row nennt.
2026-09-29
- §4.9: Umfang des Hashes festgehalten — Inhalt, Name,
role,schema, nie der Hash selbst. - §11: Merkposten „Speicher bei lang laufenden Meta-Contexts“ — Budget per Envelope, Signal vor der Grenze, Stufen Provenance → Cache → Context-Reset, Warmstart über persistenten Cache.
- §3.7 neu: Fragen als Daten — Query-Record als Vertrag zwischen JDL und Zig, Builder als Vorderseite, Schema als Daten, feste gegenüber revisionsabhängigen Fakten. Arbeitsrichtung, mit erklärendem Code und Diagrammen. Neue offene Punkte dazu in §11.
- Code-Beispiele entfernt und durch technische Beschreibungen ersetzt: §6.1 (Signaturen, jetzt als Gegenüberstellung), §7 (Codec-Engine und HTTP-Engine, jetzt „Zwei Engines im Überblick“). Die Beispiele nutzten veraltete Syntax und widersprachen in §7.2 dem Storage-Modell und der Bindungsregel. §5.4-Diagramm und §11 („Katalog“) nennen keine Namen aus den entfernten Beispielen mehr.
- Begriffe vereinheitlicht: „Deskriptor“ im Fließtext statt „Descriptor“ (Bezeichner wie
ContextDescriptorbleiben), durchgehend „contentHash“ statt „Inhaltshash“, Mehrzahl „Contexts“. §4.7: „Ein TypeDescriptor beschreibt einen Typ“ statt „Ein Deskriptor …“, weil Deskriptoren inzwischen viele Rollen haben. §4.9:hashim Kopf als contentHash ausgewiesen. - Alle Mermaid-Diagramme durch reine ASCII-Diagramme ersetzt. Inhalt und Kanten unverändert; Sequenzdiagramme sind als Ablaufbaum dargestellt.
- §4.1: Klargestellt, dass nicht jeder CompilerDB-Eintrag ein Deskriptor ist. Widerspruch zu
jade-compiler-db-spec§3.7 benannt. - §4.2: Übergang heißt jetzt „Selektion und Projektion" beziehungsweise „Laden und Zulassen". Im Artefakt liegen Excerpts. Diagramm angepasst.
- §4.3: Identitäten neu erklärt: Id, Name und Hash sprechen denselben Eintrag an, beantworten aber verschiedene Fragen.
TypeIdals typisierter Fall derDescriptorIdeingeordnet (war in §4.5 schon so angelegt).canonicalist optional; Exporte brauchen einen Namen; bei benannten Deskriptoren geht der Name in den Hash ein. - §4.4: Selektion (welche Deskriptoren, von den Wurzeln erreichbar) und Projektion (was je Deskriptor) getrennt. Begriff Excerpt eingeführt. Excerpt trägt eigenen Hash. Kopf überlebt die Projektion. Umgebungsabhängige Werte stehen nicht im Artefakt, Bindungen bleiben offen. Tabellenzeile zum Namen korrigiert: bei Exporten Pflicht.
- §4.5: Entschieden, dass Verweise über den Hash ihres Ziels laufen; Kettenreaktion der Hashes und ihr Halt an Schnittstellen beschrieben. Doppelten Absatz zum Interning entfernt. Kopf-Tabelle um die einheitlichen Felder ergänzt.
- §4.9 neu: Der Kopf eines Deskriptors —
hash,role,schema, optionalcanonical; die bauende Engine gehört in die Herkunft. Verweise über Artefaktgrenzen nur auf Schnittstellen; Zulassung über Name, dann Hash. - §3.5, §5.3, §6.4: Kopf wird vom Materializer geschrieben, nicht von der Engine.
- §5.5: ContextEngine als RuntimeEngine eingeordnet.
- §5.7: Diagramm und Text auf Excerpt und Hash-Verweise umgestellt.
- §6.4.1: Abgrenzung der View-„Projektion" von der Excerpt-Projektion.
- §6.5: Klargestellt, dass eine laufende Anwendung nicht beschreibt, sondern nur fertige Artefakte zulässt.
- §8: Diagramm um Selektion, Projektion und Kopf ergänzt.
- §10: Umgebungsunabhängigkeit des Artefakts als Eigenschaft ergänzt.
- §11: „Relokierbare Referenzen" und „Kanonische Referenzen im contentHash" durch die entschiedene Hash-Regel ersetzt; übrig bleibt „Zyklische Verweise". Neu offen: Schema-Versionen, Name und Excerpt-Hash, Deskriptorliste der CompilerDB-Spec.