Zum Inhalt

JDL — service als TypeKind, Row-Elemente und Meta-Phase

Status: Arbeitsnotiz, ausdrücklich nicht normativ Version: 0.2.0 Datum: 2026-09-06 Zweck: Den mentalen Konsens einer Gesprächsrunde an einer Stelle festhalten, bevor er sich auf mehrere Chats verteilt. Primärquellen geprüft: jdl-sprachreferenz v0.1.0, jdl-effect-rows-und-throw v0.1.0, Konsolidierung 00-ontology-types-tags-phantoms, 02-type-system-tags, 07-compilerdb-tag-subsystem, jdl-blueprint-spec, 11-bootstrap-seeding-praegung, 07-cast-effekte-refinements v0.3

Lesehinweis. Diese Notiz entscheidet nichts. Sie hält fest, worauf sich das Gespräch geeinigt hat, was ausdrücklich offen blieb, und welche bestehenden Dokumente einander widersprechen. Wo zwei Quellen sich widersprechen, steht der Widerspruch hier — nicht seine Auflösung.


0. Kennzeichnung

Marke Bedeutung
[K] Arbeitskonsens dieser Runde — tragfähig, aber nicht normiert
[B] in einer Primärquelle belegt, Fundstelle genannt
[W] Widerspruch zwischen bestehenden Dokumenten
[O] offen, ausdrücklich nicht entschieden

Syntaxbeispiele sind Anschauung. Nicht normierte Formen tragen // spekulativ. Auf spekulative Formen wird keine Folgerung gestützt.


1. Motivation

Ausgangspunkt war der Vorschlag, service vollständig abzuschaffen und Service-Verträge als Blueprints zu modellieren. Das Motiv war Reduktion: kein zweites Vertragsmodell, keine parallele Descriptor-Pipeline, keine Effect-spezifische Qualifier-Syntax.

Der Vorschlag wurde verworfen. Der tragende Grund ist der Wertstatus, nicht die Syntax.

Ein Blueprint ist ein Vertrag über einen Wert, den jemand baut. Es gibt Instanzen; sie sind serialisierbar, übergebbar, netzwerkfähig, zur Laufzeit inspizierbar (jdl-blueprint-spec §6.1).

Ein Service ist ein Vertrag über eine Umgebung, die jemand bereitstellt. Es gibt keine Instanzen: Nutzercode kann einen Handler nicht als Wert erlangen (Regel A-5, jdl-sprachreferenz §8.5). Ohne A-5 ist die Abgrenzung zu Capability[T] nicht haltbar.

Beides „Blueprint" zu nennen zwingt dazu, den Unterschied nachträglich mit Prädikaten wieder einzufangen. [K]

Ein Nebenargument aus dem Bootstrap stützt das: jdl-blueprint-spec §5.4 erzwingt Serializable auf jedem Blueprint-Feld. Alloc wird während Constructed → Seeded gebraucht (07 §7.5), und BSP-12 verbietet dem Seed, auf Stdlib-Symbole zu bauen. Ein Service-TypeKind trägt diese Pflicht nicht und braucht keine Ausnahmeregel. [B]

1.1 Zwei Argumente, die im Verlauf zurückgezogen wurden

Der Vollständigkeit halber, damit sie nicht wiederkehren:

Row-Semantik in Feldposition war als Gegenargument gedacht („ein Blueprint-Feld trägt automatisch eine Row, ein Service-Vertrag durfte das nicht"). Hinfällig: jdl-sprachreferenz §19.3 lässt Rows in Feldpositionen ausdrücklich zu und schließt damit O-7 aus 07 §11.1. [B]

Operationssignaturen ohne Row ebenfalls hinfällig: jdl-effect-rows-und-throw §2.3, Regel K-3, erlaubt abortive Effekte in der Operationssignatur und präzisiert 07 §3.1. Die Merkformel dort: Was den Aufrufer abbricht, steht bei der Operation. Was der Handler zum Arbeiten braucht, steht beim Handler. [B]


2. Kernmodell

2.1 service wird ein TypeKind [K]

Ein Service-Vertrag wird über die bestehende TypeBody-Grammatik deklariert, neben struct, enum, union, blueprint. Das Top-Level-Keyword service entfällt als eigene Deklarationskategorie.

// spekulativ — Deklarationsform noch nicht normiert
type Db : service {
    query: (str) ->{Throw[DbError]} [Row]
    exec:  (str) ->{Throw[DbError]} u64
}

jdl-blueprint-spec §4.1/§4.2 hat die Erweiterungsstelle bereits vorgesehen: TypeBody und die ValueTypeKind-Liste sind die Orte, an denen ein Kind hinzukommt. [B]

2.2 Was der Kind trägt [K]

Drei Eigenschaften, die vorher am Konstrukt service hingen und jetzt am Kind hängen:

  • Nicht inhabitiert. Es gibt keine Werte dieses Typs, nie. A-5 wird damit eine Kind-Eigenschaft statt einer Sonderregel.
  • Keine Serializable-Pflicht auf Feldern. Die hängt am Blueprint-Kind.
  • Nur Row-Position. Nicht Parameter-, nicht Rückgabeposition, nicht Typ-Constraint. Das erhält die Invariante aus 04-qualifier §3.4 und jdl-sprachreferenz §11.1: Protocols erscheinen nicht in der Row, Services nicht als Typ-Constraint.

2.3 Was dadurch entfällt [K]

eigene Deklarationsgrammatik für service
ServiceId als eigener ID-Space neben TypeId
spezielle Tag-/Qualifier-Syntax in Rows
per-Domäne-Service-Engines (DbServiceEngine, FsServiceEngine)
ServiceInterfaceSpec als separates Vertragsmodell

Die letzten beiden folgen aus jdl-design-note §2.2 (Engines generisch halten, nicht per Domäne) und aus der Beobachtung, dass ein Vertrag aus Funktionssignaturen sich strukturell validiert, nicht domänenspezifisch.

Konkret betroffen im Implementierungsmodell: 07-compilerdb-tag-subsystem §3 führt SymbolKind.service mit eigener ServiceId im Payload-Union, §5 zusätzlich TypeLevelValueKind.service. Beides würde zu einem TypeKind zusammenfallen. [B]

2.4 Die Row ist eine Bedingung, kein Auftrag [B]

Ein Row-Element beauftragt niemanden. Es ist eine Bedingung an die Aufrufstelle — das Bild aus jdl-sprachreferenz §7.1 ist eine Schuld, die abgetragen wird, bis in main die leere Menge steht.

Praktische Folge: Es gibt keinen Zeitpunkt, an dem jemand nach einem Handler sucht.


3. Row-Modell

3.1 Elementarten

jdl-effect-rows-und-throw §3.1 definiert, markiert [N]: Ein Element ist entweder ein getaggter Service oder ein Fehlerkanal Throw[E]. [B]

Der Arbeitskonsens unterscheidet zwei Achsen, die bisher vermischt wurden:

Verhalten — wodurch wird das Element getilgt, was passiert beim Lowering:

Art Tilgung Lowering
Requirement Handler-Installation Evidence-Feld, indirekter Call
Abortive catch an der Aufrufstelle bestehendes Unwinding

Herkunft — wer darf es in die Welt setzen:

Herkunft Beispiele
deklarierbar service-Typen: Db, Fs, Alloc[A]
eingebaut Sprachkonstrukte: Throw[E]

Die Herkunftsachse ist keine Erfindung dieser Runde. Regel E-1′ (jdl-effect-rows-und-throw §2.7) sagt bereits: Throw[E] ist ein Row-Element, kein deklarierbarer Service. [B]

Nutzen der Trennung: Eine neue eingebaute Art ist ein Eintrag in einer geschlossenen Aufzählung im Trusted Core, kein Umbau der Algebra. Ein geschlossenes Enum zwingt außerdem jede Stelle, die Row-Elemente behandelt, zur vollständigen Fallunterscheidung — kommt eine weitere Art dazu, meldet sich der Compiler dort, wo sie fehlt. Bei einem offenen Eigenschaftsset meldet sich niemand. [K]

Meta fällt aus dieser Tabelle heraus. Siehe §8.

3.2 Identität und Normalisierung [B]

Bestehend und unverändert übernommen aus jdl-effect-rows-und-throw §3:

  • Zwei Elemente sind identisch bei gleichem Servicenamen und Tag beziehungsweise gleichem Fehlertyp. Db[Primary] ≠ Db[Replica].
  • Vier Grundoperationen: Union, Subset, Difference, Normalisierung (§3.2).
  • Throw verteilt über strukturelle Unions, nicht über nominale Typen (Y-1, §3.3).
  • Rows mit Throw[V] für ungebundene Typvariablen sind nicht normalisiert; Normalisierung läuft nach der Substitution (Y-2, §3.4).
  • Beschränkung von Row-Variablen auf nicht-abortive Rows existiert; Schreibweise offen (Y-3, §3.5).

Für den service-TypeKind kommt eine Einschränkung dazu, die den Solver schützt: Ein Row-Element muss syntaktisch ein Vertragsname oder eine Anwendung darauf sein — kein berechneter Vertrag, keine TypeFn-Ausgabe. Sonst ist Element-Identität erst nach Meta-Auswertung entscheidbar, und die Normalisierung wandert in den heißen Pfad. [K]

Row-Aliase sind davon ausgenommen; siehe §9.


4. Rollen und Phantom-Tags

4.1 Db[Primary] als echte Type-Level-Anwendung [K]

Der Konsens: Wenn es wie Typanwendung aussieht, soll es Typanwendung sein — keine Effect-spezifische Qualifier-Syntax.

Konzeptionell:

Apply(
    TypeId(Db),
    TagId(Primary)
)

Das Ergebnis trägt wieder eine kanonische Requirement-/Typidentität. Die genaue interne Repräsentation ist [O], siehe §4.4.

4.2 Rollen kommen aus geschlossenen Tag-Domains [K]

// spekulativ
tag DbRole : enum = | Primary | Replica

type Db[phantom role: DbRole] : service { ... }

Begründung: Die Ontologie kennt tag X (einfach) und tag D : enum (geschlossene Domain, 00-ontology §5). Eine offene Position „irgendein Tag" wäre ein neues Konzept. Die geschlossene Domain kauft die Diagnostik — der Compiler kann die gültigen Rollen auflisten (02-type-system-tags §3). [B]

phantom ist die bestehende Mechanik: Teil der Typidentität, kein Layout, keine Runtime-Repräsentation (T-7, T-8). [B]

4.3 Rollenpolymorphie [K]

Rollenpolymorphie in Signaturen ist belegt: def build[a](...) ->{Alloc[a]} Buf[a] (jdl-sprachreferenz §10.3). Ob dieselbe Variable an der Aufrufstelle qualifizieren darf, ist offen und ist der kritischste offene Punkt der ganzen Notiz — siehe §7.1.

4.4 TypeId gegen TagId [W] [O]

Zwei Primärquellen widersprechen sich direkt.

jdl-sprachreferenz §5.8: Ein Tag ist ein Typ ohne Werte.

07-compilerdb-tag-subsystem §2: TagId darf nicht TypeId sein; separate ID-Spaces oder mindestens stark typisierte Wrapper; ausdrücklich kein isTag-Flag am TypeDescriptor, weil das Tags zu Pseudo-Types macht.

Die Wahl ist nicht kosmetisch:

  • Tag ist Typ → Db[Primary] ist triviale Typanwendung, ein Mechanismus.
  • TagId getrennt → Db[Primary] ist eine gemischte Anwendung eines service-TypeKinds auf ein Phantom-Tag-Argument. Sauberer in der Ontologie, aber die Type Engine braucht eine Anwendungsform mit zwei Argumentarten.

Die Gesprächsrunde tendiert zur konsolidierten Sicht (getrennte ID-Spaces). Entschieden ist nichts. Die Type Engine wird einmal gebaut.

4.5 Constraint-Grammatik gibt Tag-Domains nicht her [W]

jdl-sprachreferenz §13.1: KindConstraint ::= Protocol ("+" Protocol)*. Für [phantom role: DbRole] braucht es eine Erweiterung.

§13.4 umgeht das derzeit mit type UserPhase: enum — also einem Werttyp an einer Stelle, an der 00-ontology §6 einen tag ... : enum fordert. Hängt mit §4.4 zusammen und sollte gemeinsam entschieden werden.


5. Handler

5.1 Schreibweise der Bindung [O], Richtung [K]

Die Klammer nach dem Vertragsnamen ist durch die Rolle belegt. Damit kollidiert provide Db[PostgresHandler] (jdl-sprachreferenz §8.6) mit Db[Primary]: dieselbe Position, zwei Bedeutungen.

Die Runde tendiert zu provide Db for PostgresHandler.

Das ist kein neuer Streit, sondern schließt einen alten: jdl-sprachreferenz §21 führt die Sache bereits als Lücke 5 („zwei Schreibweisen in Umlauf"), und 05-modulsystem-und-ffi §3.3 sowie 06-generator Invariante 10 fordern die for-Form normativ. [B]

Preis, offen benannt: Protocol-Bindung (provide Equatable for User) und Service-Bindung sähen dann gleich aus. Für die Type Engine eindeutig, weil sie den Kind von Db kennt. Für den Leser eine Unschärfe.

5.2 Der Handler-Vertrag [B]

Unverändert aus jdl-effect-rows-und-throw §2:

  • Abortivität ist Eigenschaft der Operation, nicht des Handlers (K-1, K-2).
  • Der Handler-Rumpf darf keine abortiven Effekte enthalten, die die Operation nicht deklariert (K-4).
  • Nicht-abortive Effekte des Handlers erscheinen an der Installationsstelle, nicht beim Aufrufer (K-3).

Daraus folgt die Row-Transformation, die das Modell praktisch macht: Der Consumer fordert {Db}; nach Installation eines Postgres-Handlers steht dort {Net, Tls}. Ein In-Memory-Handler erfüllt Db ohne Nachforderung.

5.3 Transportfehler brauchen kein neues Konstrukt [K]

Aus K-4 folgt: Ein Bridge-Handler, dessen Vertrag keinen passenden Fehlerkanal hat, ist nicht installierbar. Kein :> Remotable-Refinement nötig.

Die Formulierung wandert damit von „Transport ist Implementierungsdetail" zu:

Topologie ist Implementierungsdetail. Fallibilität ist Vertragsbestandteil.

5.4 Mehrfach-Erfüllung [K]

Ein Handler muss Evidence für eine feste Menge von Service-TypeIds bereitstellen können.

Begründung: Fähigkeitsunterschiede zwischen Backends werden durch Vertragsaufteilung gelöst (Db, DbTx, DbUpsert), womit Feature-Detection zur Typprüfung wird. Ohne Mehrfach-Erfüllung erzwingt diese gute Idee drei künstlich getrennte Installationen desselben Backends.

Bewusst nicht vorweggenommen: ob das syntaktisch eine einzige provide-Deklaration ist. [O]


6. Context-Binding und Evidence

6.1 Statische Materialisierung [K]

Die Formulierung, auf die sich die Runde geeinigt hat:

Die Handler-Auswahl ist vor Beginn der betreffenden Ausführung statisch materialisiert.

Nicht „zur Compile-Zeit verdrahtet" — für JadeOS soll Placement später als Source-Compile-Time entscheidbar bleiben. Entscheidend ist nur: Wenn ein Task startet, findet keine Handler-Suche mehr statt.

Das hält L-2 (Evidence Passing) und A-6 (keine Laufzeit-Handler-Map als Auflösungsmechanismus) unangetastet und stimmt mit G-4 aus jdl-effect-rows-und-throw §10.2 überein: Das Unwind-Ziel steht statisch fest, es muss nur erreicht werden. [B]

6.2 Materialisierungsgrenze [K], Ort [O]

Damit „statisch materialisiert" nicht weich wird, braucht es einen benennbaren Zeitpunkt, ab dem die Bindung eingefroren ist.

vor der Grenze    Build, Deployment, Context-Instantiation — beliebig
nach der Grenze   nur noch Konfiguration, nie Auswahl

Wo diese Grenze konkret liegt, ist offen.

Folge für den Codegen, die nicht implizit bleiben sollte: Wenn Materialisierung erst bei Context-Instantiation läuft, darf der Evidence-Vektor nicht als Konstante in die Aufrufstellen eingebacken werden. Mit L-2 verträglich — ein versteckter Parameter kann spät befüllt werden — aber es ist eine Zusage. [K]

6.3 Coherence [K]

Innerhalb eines Contexts existiert für jedes Requirement genau eine Evidence. Keine Reihenfolge, kein „innerster gewinnt", kein Registry-Verhalten.

Für Aufrufstellen steckt das bereits in A-2 und A-3 (jdl-effect-rows-und-throw §4.2). Für die Context-Materialisierung sollte es ausdrücklich gelten. [B] für A-2/A-3, [K] für die Erweiterung.

6.4 Drei getrennte Ebenen [B]

Handler-Typ        Compile-Zeit
Evidence           Materialisierungszeit
Handler-State      Runtime

Das ist nicht neu, sondern belegt: jdl-sprachreferenz §8.7 zeigt im env:-Block Db[Primary]: PostgresHandler { pool } neben Db[Replica]: PostgresHandler { replicaPool } — ein Handlertyp, zwei Rollen, verschiedene Konfiguration.

Diese Trennung ist zugleich der Grund, warum A-5 haltbar bleibt: Der Nutzer will nie den Handler in der Hand halten, er will den Connection-String setzen. Konfiguration ist ein normaler Wert.


7. Generics und Bibliotheksfähigkeit

Dieser Abschnitt entstand am Testfall „ein ORM/ODM, das mehrere Datenbanken bedient". Ergebnis: kein Sprachproblem, aber vier Punkte, die eine Bibliothek erst sichtbar macht.

7.1 Tag-Variablen an der Aufrufstelle [O] — kritischster Punkt

A-3 macht Qualifikation verpflichtend, sobald eine Row-Variable im Spiel ist. Jede row-polymorphe Bibliotheksfunktion hat eine. Gleichzeitig ist eine ORM-Funktion in der Rolle polymorph.

Sie muss also sinngemäß mit variabler Rolle qualifizieren können:

// spekulativ
def findById[T, r](id: Id) ->{Db[r], Throw[DbError]} T

Signaturseitig ist Rollenpolymorphie belegt (§10.3). Zur Aufrufstelle sagen die Quellen nichts.

Ohne diese Zusage ist keine rollenpolymorphe Bibliotheksfunktion schreibbar — jedes ORM müsste pro Rolle dupliziert werden. Das wäre kein Schönheitsfehler, sondern ein Ausschlusskriterium.

7.2 Offene Rows [B]

Unverändert: A-7 (§4.3) — ein Handler, auch catch, darf nur namentlich bekannte Elemente eliminieren. Bei offener Row muss die eliminierte Menge explizit benannt werden.

7.3 Backend-variable Typen gehören an den Requirement-Typ [K]

Ein Vertrag kann keine Typen haben, die erst der Handler festlegt — der Row-Check läuft vor der Handler-Auflösung. Was pro Backend variiert, muss Parameter am Row-Element sein:

// spekulativ
type Db[Dialect, phantom role: DbRole] : service { ... }

Das ist ehrlich statt einschränkend: Code, der SQL-Strings baut, ist an SQL gekoppelt. Sichtbar bleibt die Kopplung nur in der untersten Schicht, weil Anwendungscode Domänenverträge fordert (->{Users}), nicht Db.

Randbemerkung zur Ontologie: Rollen sind Tags, Dialekte sind Typen. Ein Tag kann kein Protocol implementieren (T-4), und an einem Dialekt hängt Verhalten. [B]

7.4 Kopfposition immer konkret [K]

Db[a] ja, f[Primary] nein. Solange der Kopf konkret ist, ist die Unifikation erster Ordnung und billig. Eine Variable in Kopfposition führt in Higher-Order-Unifikation, im allgemeinen Fall unentscheidbar.


8. Meta-Phase

8.1 Der Widerspruch [W]

jdl-sprachreferenz §15.1, als Entscheidung markiert:

typefn entfällt; Compile-Zeit-Auswertung ist def mit Meta in der Row.

jdl-effect-rows-und-throw §3.1, markiert [N]:

Ein Element ist entweder ein getaggter Service oder ein Fehlerkanal Throw[E].

Die Throw-Notiz ist die neuere (05.09.), führt die Sprachreferenz als Quelle und hat Meta übersehen. §3.1 ist als normativ markiert und damit gerade nicht offen. Der Nachzug gehört in §14 der Throw-Notiz.

Zusätzlich merkt jdl-sprachreferenz §15.1 selbst an, dass 08-grammar-sketch §6 und jdl-typefn-spec weiterhin typefn führen. Der Meta-Ansatz ist also selbst noch frisch.

8.2 Einordnung: eingebaut, nicht deklarierbar [K]

Konsens: Meta ist ein eingebautes Sprachkonstrukt, kein user-deklarierbarer Service. Dieselbe Kategorie wie Throw[E] nach E-1′.

8.3 Die Phase steht vor der Row [K]

Meta verhält sich nicht wie ein Element. Es sagt nicht „diese Funktion braucht etwas", sondern „diese Funktion läuft in einer anderen Phase". An der Algebra ist das ablesbar: Es gibt keinen Handler, keine Installation, keine Difference-Operation, und die Abschwächung von ->{} zu ->{Meta} bedeutet nichts.

Die Phase wandert deshalb vor die Row:

// spekulativ — Schreibweise nicht normiert
def Share(policy: SharePolicy) ->Meta{} MetaRecord

Was das leistet:

  • Die Elementarten bleiben zwei. jdl-effect-rows-und-throw §3.1 wird wieder korrekt und braucht keinen Nachzug.
  • ->{g, Meta} kann nicht entstehen. Eine Row-Variable steht für Elemente, nicht für Phasen.
  • Pure und Deterministic bleiben unberührt. Labels lesen die Row, die Phase steht daneben. P-1 wird nicht angefasst.
  • Die Frage aus §8.6 wird aussprechbar statt zur Ja/Nein-Regel.

Konzeptionell ist die Phase damit eine eigene Achse des Funktionstyps:

FunctionType = Params × Phase × Row × Result

Offen bleibt eine Parserfrage: ob ->Meta{...} gegen -> Meta als Rückgabetyp eindeutig liest. [O]

Nachzug: jdl-sprachreferenz §15.1 formuliert „mit Meta in der Row" und ist auf „mit Meta-Phase am Pfeil" umzustellen. Die Entscheidung selbst — typefn entfällt — bleibt unangetastet.

8.4 Genau zwei Phasen, Runtime implizit [K]

Die Achse beantwortet genau eine Frage: Läuft der Rumpf dieser Funktion während der Übersetzung oder während der Ausführung? Darauf gibt es zwei Antworten. Eine dritte ist nicht denkbar — sie gehörte zu einer anderen Frage.

Das ist die tragende Begründung, nicht Sparsamkeit. „Man könnte jede weitere Phase in beiden abbilden" würde genauso gut rechtfertigen, auf eine Phase zu kollabieren.

Default. Analog zu R-3 („eine fehlende Effektmenge bedeutet die leere Menge, nicht eine unbestimmte") bedeutet eine fehlende Phasenangabe Runtime, nicht „irgendeine". def f(x: i32) -> i32 ist exakt def f(x: i32) ->Runtime{} i32.

Damit ist Meta markiert und Runtime unmarkiert. In fast allem Code ist die Phase unsichtbar. Ob Runtime überhaupt schreibbar ist oder nur implizit existiert, ist eine Oberflächenfrage. [O]

Der Bootstrap-Kandidat fällt weg. BootstrapSeedBuild ist keine dritte Phase. BSP-6 sagt, das Seed-Backend erzeugt Deskriptoren und keinen ausführbaren Bytecode, und der Bootstrap-Checker verbietet Funktionskörper, Closures und Effekte. Wo keine Funktionskörper sind, gibt es nichts zu phasenmarkieren — es ist ein Compiler-Modus, keine Phase eines Funktionstyps. [B]

Dasselbe Argument trifft die Materialisierungsgrenze aus §6.2 und den Plugin-Loader: Beide beantworten „wann steht die Bindung fest" beziehungsweise „wann wird geladen", nicht „wann läuft der Rumpf". Andere Achsen, nicht weitere Werte auf dieser.

Kosten im Trusted Core. Eine Dimension mit zwei Werten, Default und ohne Variable braucht in der Unifikation genau eine Operation: Gleichheit. Keine Ordnung, keine Teilmengenprüfung. Eine spätere dritte Phase wäre additiv — bestehender Code bliebe gültig.

8.5 Aufrufrichtungen [K]

Meta    → Meta       erlaubt
Runtime → Runtime    erlaubt
Meta    → Runtime    erlaubt, unter Bedingung (§8.6)
Runtime → Meta       kein Aufruf, sondern Materialisierung

Meta darf Runtime aufrufen. Das löst den Großteil der Wiederverwendungsfrage: Erststufige Hilfsfunktionen — Arithmetik, Stringoperationen, clamp — sind in beiden Phasen nutzbar, ohne dass jemand sie markiert oder dupliziert. Das ist Voraussetzung dafür, dass die Default-Wahl aus §8.4 trägt: Wenn Runtime unmarkiert ist, kann die Lösung nicht darin bestehen, dass Autoren etwas markieren.

Runtime darf Meta nicht aufrufen. Der Grund ist nicht, dass der Laufzeitumgebung ein Compiler fehlt — das wäre ein praktisches Argument, das sich mit einem Interpreter erschlagen ließe. Der Grund ist, dass die Antwort schon existieren muss: Ein Refinement wird in den Typ hineinmaterialisiert, bevor das Programm läuft; ein TruthProfile ist ein Beweis, der vor der Ausführung geführt wurde. Hinge ein Meta-Ergebnis von einem Laufzeitwert ab, wäre das Descriptor- und Beweismodell zirkulär. Ein Interpreter machte die Kante mechanisch möglich und bräche das Modell trotzdem.

Die vierte Zeile ist kein Aufruf. type X : struct { ... } :> Share(Local) ist die Kante von Runtime nach Meta, aber Materialisierung: Die Meta-Funktion läuft während der Übersetzung, ihr Ergebnis wird ins Meta-Record gemerged. Wer das als Aufruf modelliert, baut eine Kante ins Modell, die es nicht gibt.

Folge: es entsteht doch eine Abschwächungsrichtung. Wenn Meta-Code Runtime-Funktionen aufrufen darf, ist im Meta-Kontext ein Runtime-Funktionswert dort brauchbar, wo ein Meta-Funktionswert erwartet wird. Einseitig, Runtime nach Meta, nie zurück. Anders als bei der Row folgt sie nicht aus einer Mengenordnung, sondern aus der Aufrufregel. [K]

8.6 Was in der Meta-Phase erfüllbar ist [O] — Vorfahrtsfrage

Nicht jede Runtime-Funktion ist zur Übersetzungszeit ausführbar. Eine Funktion mit ->{Db} offensichtlich nicht: In der Meta-Phase gibt es keinen Db-Handler.

Die Regel aus §8.5 braucht deshalb eine Bedingung, und sie lautet vermutlich:

Meta darf eine Runtime-Funktion aufrufen, wenn deren Row in der Meta-Phase erfüllbar ist.

Damit sind zwei bisher getrennte Fragen dieselbe: Welche Requirements sind in der Meta-Phase erfüllbar und was darf Meta aufrufen. Was erfüllbar ist, entscheidet, was aufrufbar ist.

jdl-sprachreferenz §15.2 begründet nur, dass Compile-Zeit-Code keinen Laufzeitzustand berühren darf. Das ist eine Stage-Kompatibilitätsregel, kein absolutes Requirement-Verbot.

Alloc hat Vorfahrt. Auch der kleinstmögliche Meta-Umfang alloziert — ein Meta-Record bauen, Felder durchgehen, einen Typausdruck zusammensetzen. Ohne Antwort steht der Evaluator, egal wie klein er ist.

// spekulativ — offen, keine Empfehlung
def Flatten[T]() ->Meta{Alloc[MetaArena]} Type

Die Form der Antwort formt spätere Erweiterungen mit: Ist Meta-Allokation ein eigener Arena-Tag, der am Ende der Übersetzung fällt, ist Aufdrehen billig. Wird sie am Compiler-Allokator vorbeigeschmuggelt, wird jede Erweiterung eine Nachverhandlung.

Abgeleitet, nicht deklariert. Die Eignung einer Funktion für die Meta-Phase sollte aus ihrer Row folgen, nicht aus einer Markierung. Das passt zu P-0 (Labels werden bewiesen, nicht gesetzt) und spart Rusts const fn-Annotation. Preis, offen benannt: Rust markiert auch deshalb, weil es eine API-Zusage ist. Bei Ableitung kann ein Bibliotheksautor die Meta-Nutzbarkeit brechen, indem er ein Row-Element hinzufügt, ohne es zu bemerken. [O]

8.7 Throw in der Meta-Phase [O]

Eine Compile-Zeit-Funktion, die eine Diagnostik erzeugt, ist plausibel. Ob der Abbruch dort Throw[E] ist — mit Compile-Fehler statt Unwinding — oder ein eigener Kanal, ist offen.

8.8 Umfang: der Regler, nicht der Schalter [K]

Was jetzt festliegt, ist die Form — Phase als Achse, zwei Werte, Runtime implizit, Aufrufrichtung. Das ist der Teil, der später teuer zu ändern wäre, weil Signaturen davon abhängen.

Was offen bleibt, ist der Umfang: wie viel davon der Meta-Evaluator tatsächlich ausführt. Er startet klein — Typausdrücke, Verzweigung, Musterabgleich auf Typebene, also der bisherige typefn-Umfang aus jdl-sprachreferenz §15.2 — und wird schrittweise aufgedreht.

Zwei Dinge, die dabei sofort gelten müssen:

Die Grenze liegt im Evaluator, nicht in der Sprache. Das wird an der Diagnostik sichtbar. „Das kann die Meta-Phase nicht" ist eine Sprachaussage und für immer wahr. „Das kann der Meta-Evaluator noch nicht" ist eine Implementierungsaussage und veraltet planmäßig. Die Fehlermeldungen müssen von Anfang an die zweite Form haben, sonst zementieren sie eine Grenze, die niemand ziehen wollte.

Der Evaluator braucht ein Budget. Eine ->{}-Funktion darf laut 07 §6.5 ewig schleifen. Zur Übersetzungszeit heißt das: Der Compiler hängt. Schrittzähler plus eigene Diagnostik, mehr nicht — aber es gehört von Anfang an dazu.

Warum der Regler überhaupt lohnt. Nicht „bringt dem Self-Hosting näher" — der selbstgehostete Compiler ist ein gewöhnliches Runtime-Programm ohne Phasenmarker (07 §7.4). Das tragende Argument ist ein anderes: Je mehr die Meta-Phase kann, desto weniger muss im Compiler stehen. Derive, Wire-Ableitung, Parser-Generatoren, Blueprint-Validierung sind bei anderen Sprachen Compiler-Pässe und wären hier Bibliotheken. Das reduziert die Fläche, die überhaupt gehostet werden muss, und ist zugleich das tragende Argument für das Plugin-Modell.

Der Preis steigt mit dem Regler, und er wird in Zig bezahlt. Wer die Meta-Phase weit aufdreht, braucht einen Interpreter für einen großen Teil der Sprache — beim ersten Compiler in Zig, bis der selbstgehostete übernehmen kann. Zig macht das mit comptime genau so. Jede Reglerstufe ist reversibel nach oben, keine nach unten: Einen zu großen Meta-Evaluator wird man nicht wieder los.

Kalibrierung. Die ersten echten Meta-Funktionen sind die Refinements, die der Bootstrap Seed braucht — Share, Own, Memory, Derive. Sie sind klein und kommen ohnehin zuerst. Der Anfangsumfang lässt sich damit an einer echten Anforderung messen statt an einer Vermutung.

8.9 Phasenvariable an Parameterpositionen [O] — nicht jetzt

Was §8.5 nicht löst: den Fall, in dem der Callback Meta ist.

def map[T, U, g](xs: [T], f: (T) ->{g} U) ->{g} [U]

f deklariert implizit Runtime-Phase. Ein Meta-Callback passt dort nicht hinein, unabhängig davon, wer wen aufruft — das Problem sitzt in der Parameterposition, nicht an der Aufrufstelle. Erklärt man maps Callback als Meta, kippt map selbst in die Meta-Phase, und das Duplikat ist wieder da.

Die ehrlichste Antwort wäre eine Variable über die Phase, strukturell dasselbe wie g für die Row:

// spekulativ — nicht beschlossen
def map[T, U, g, p](xs: [T], f: (T) ->p{g} U) ->p{g} [U]

Nicht zu verwechseln mit Zigs comptime. Zigs comptime an einem Parameter verlangt, dass ein Argument zur Compile-Zeit bekannt ist. Hier geht es um die Phase, in der ein Funktionsparameter läuft. Zig kann das nicht ausdrücken, weil dort die Phase keine Eigenschaft des Funktionstyps ist.

p ist kein phantom. Der Test: Wäre p phantom, müsste reified p denkbar sein. Ist es nicht — eine Phase lässt sich nicht materialisieren. phantom beantwortet, ob ein Parameter zur Laufzeitrepräsentation eines Werts beiträgt; bei einer Phase stellt sich die Frage nicht. p ist eine Variable über eine andere Sorte, genau wie g, und g schreibt auch niemand als phantom g.

Was dabei auffällt: Mit p gäbe es zwei implizit sortierte Variablen in derselben Parameterliste. Bisher ging das gut, weil es genau eine implizite Sorte gab. Bei zwei muss der Leser aus der Verwendung zurückschließen. Die Sorten gehörten damit in die Parameterliste — und das ist dieselbe Grammatikerweiterung, die offener Punkt L ohnehin verlangt (jdl-sprachreferenz §13.1 lässt als Constraint nur Protocols zu). Eine Erweiterung, drei Verwendungen: role: DbRole, g: Row, p: Phase.

Reihenfolge, bewusst so und nicht anders:

1. Alloc in der Meta-Phase entscheiden (§8.6)
2. erste Meta-Funktionen tatsächlich schreiben
3. sehen, wo es klemmt
4. bei Bedarf zwei, drei Meta-Kombinatoren duplizieren
5. wenn die nerven: p

Das ist keine Vertagung aus Vorsicht. p ist eine Antwort auf eine Erfahrung, die noch nicht gemacht wurde — und es ist jederzeit additiv nachrüstbar, ohne bestehenden Code zu brechen.

8.10 Folgen für den Trusted Core [O]

Die Phase in Modul-Metadaten. Sie ist noch stärker löschbar als die Row — eine Meta-Funktion erreicht den Generator nie. Aber sie muss in die Modul-Metadaten, aus demselben Grund wie die Row (07 §7.4).

Und der Rumpf dazu. Hier kippt eine bestehende Eigenschaft. 07 §7.4 begründet die Row in den Metadaten damit, dass ein Aufrufer in einem anderen Modul sie prüfen kann, ohne den Rumpf zu sehen. Für Meta→Runtime über Modulgrenzen braucht der Compiler aber genau den Rumpf, um ihn auszuführen.

Zwei Auswege, beide mit Kosten: Rümpfe geeigneter Funktionen wandern ins Modul-Artefakt, oder die Regel gilt nur dort, wo der Rumpf ohnehin sichtbar ist. Das ist eine Entscheidung am Artefaktformat — die Sorte, die vor dem Generator fällt, nicht danach. Verwandt mit G-7 aus jdl-effect-rows-und-throw §10.4.


9. Row-Aliase

[K] Ein Row-Alias expandiert statisch auf eine feste Menge konkreter Elemente, vollständig vor jeder Unifikation.

DbFull = { Db, DbTx, DbUpsert }

Ohne so etwas trägt jede ORM-Signatur fünf Row-Elemente.

Nicht zulässig: berechnete Row-Verträge, also eine Form, deren Elementmenge erst durch Meta-Auswertung entsteht. Sonst wandert Meta-Auswertung mitten in die Row-Normalisierung, mit CompilerDB-Zyklen als Zugabe.

Das ist die Ausnahme zur Einschränkung aus §3.2: Expansion auf konkrete Elemente ja, Berechnung nein.


10. Invarianten dieser Runde

Dinge, die nach dem Arbeitskonsens ausdrücklich nicht passieren dürfen. Keine davon ist normiert.

S-1  Ein service-Typ hat keine Werte. Nutzercode erlangt nie einen Handler.
S-2  Ein service-Typ steht nur in Row-Position.
S-3  Service-Identität liegt im normalen TypeId-Raum.
S-4  Es gibt keine dynamische Handler-Suche zur Laufzeit.
S-5  Handler-Auswahl ist vor Ausführungsbeginn materialisiert und danach eingefroren.
S-6  Pro Requirement genau eine Evidence im Context.
S-7  Ein Handler führt keine abortiven Effekte ein, die die Operation nicht deklariert.
S-8  Row-Elemente sind syntaktisch Vertragsnamen oder Anwendungen darauf;
     keine berechneten Verträge.
S-9  Row-Aliase expandieren statisch und vollständig vor Unifikation.
S-10 Die Kopfposition einer Row-Element-Anwendung ist konkret.
S-11 Ein Handler kann Evidence für mehrere Service-TypeIds gemeinsam bereitstellen.
S-12 Es gibt genau zwei Phasen. Fehlende Angabe bedeutet Runtime, nicht "beliebig".
S-13 Runtime-Code ruft niemals Meta-Code auf.
S-14 Meta-Code ruft Runtime-Code nur auf, wenn dessen Row in der Meta-Phase
     erfüllbar ist.
S-15 Die Eignung für die Meta-Phase wird abgeleitet, nicht deklariert.
S-16 Die Grenze des Meta-Umfangs liegt im Evaluator, nicht in der Sprache;
     Diagnostik formuliert entsprechend.
S-17 Der Meta-Evaluator terminiert unter Budget.

11. Offene Punkte

# Frage Stand
A Dürfen Tag-Variablen an der Aufrufstelle qualifizieren? offen — Ausschlusskriterium für Bibliotheken
B Syntaktische Form der Mehrfach-Erfüllung semantisch geklärt, Syntax offen
C Row-Aliase geklärt: nur feste Expansion
D Backend-variable Typen als Vertragsparameter geklärt
E Kopfposition konkret geklärt
F Coherence des Context-Bindings geklärt
G Handler-Typ / Evidence / State belegt, jdl-sprachreferenz §8.7
H Meta vor der Row statt als Row-Element geklärt (§8.3)
I Welche Requirements sind in der Meta-Phase erfüllbar? offen — Vorfahrt, Alloc zuerst (§8.6)
J Wo genau liegt die Materialisierungsgrenze? offen
K TypeId gegen TagId — ist ein Tag ein Typ? Widerspruch, offen
L Sorten in der Parameterliste (Tag-Domains, später Row, Phase) Grammatikerweiterung nötig
M Schreibweise der Handler-Bindung Richtung for, nicht entschieden
N Throw in der Meta-Phase offen
O Parst ->Meta{...} eindeutig gegen -> Meta? offen
P Default-Phase und Anzahl der Phasen geklärt: zwei, Runtime implizit (§8.4)
Q Abschwächungsrichtung zwischen Phasen geklärt: einseitig Runtime → Meta (§8.5)
R Reine Hilfsfunktionen in beiden Phasen weitgehend geklärt durch §8.5; Rest in §8.9
S Darf Meta-Code Runtime aufrufen? geklärt ja, Bedingung ist I (§8.5)
T Ist BootstrapSeedBuild eine dritte Phase? geklärt: nein, Compiler-Modus (§8.4)
U Phasenvariable p an Parameterpositionen offen, bewusst vertagt (§8.9)
V Wandern Rümpfe meta-tauglicher Funktionen ins Modul-Artefakt? offen (§8.10)
W Bricht ein neues Row-Element still die Meta-Nutzbarkeit einer Bibliothek? offen (§8.6)

Vorfahrt. I blockiert praktisch alles Weitere an der Meta-Phase: Auch der kleinste Evaluator alloziert. Danach kommen die ersten echten Meta-Funktionen (die Seed-Refinements), und erst deren Erfahrung entscheidet über U.

Nicht blockierend, aber teuer, wenn spät: V hängt am Artefaktformat und gehört damit in dieselbe Kategorie wie G-7 — vor den Generator, nicht danach.

Nicht als Blocker geführt, aber betroffen: jdl-sprachreferenz Lücke 8 (Blueprints effektfrei gegen Rows in Feldpositionen) und jdl-effect-rows-und-throw N-7 sind derselbe offene Punkt und berühren die Blueprint/Service-Grenze.


12. Betroffene bestehende Specs

Nachziehliste bei Annahme des Modells, nach Aufwand geordnet.

jdl-effect-rows-und-throw — §3.1 gegen Meta prüfen (entweder Element ergänzen oder auf die Phasenform umstellen); §14 um diesen Eintrag erweitern.

jdl-sprachreferenz — §8.1 auf die TypeKind-Deklarationsform umstellen; §8.6 und Lücke 5 mit der Bindungsschreibweise schließen; §13.1 um Tag-Domains als Constraint erweitern; §13.4 von type UserPhase: enum auf eine Tag-Domain umstellen; §15.1 auf die gewählte Meta-Form abgleichen; §5.8 gegen §4.4 dieser Notiz prüfen.

07-compilerdb-tag-subsystem — §3 SymbolKind.service und ServiceId, §5 TypeLevelValueKind.service auf den TypeKind umstellen. Hängt an K.

jdl-blueprint-spec — §4.1/§4.2 um den Kind erweitern.

05-modulsystem-und-ffi — §4.3 baut den FFI-ServiceLayer auf der alten service-Deklaration auf; Beispiele umschreiben. Semantisch unverändert.

06-generator-und-code-emission-v2 — §8.2 ServiceInterfaceSpec und Invarianten 8/10 gegen das neue Modell prüfen.

11-bootstrap-seeding-praegung — Kategorie Core-Service-IDs (S-1 aus 07) auf TypeIds umstellen; Schema-Änderung mit schemaVersion-Erhöhung.

08-grammar-sketch, jdl-typefn-spec — bereits als überholt markiert (jdl-sprachreferenz §15.1, §22.1); die Meta-Frage entscheidet, wie sie ersetzt werden.

07-cast-effekte-refinements §7.4 — die Tabelle „wo der Code liegt" führt die Effect Row in Modul-Metadaten als persistent und versioniert. Die Phase muss mitreisen; ob auch Rümpfe meta-tauglicher Funktionen dorthin gehören, ist offener Punkt V.

05-vm-instruction-set, 06-generator, 13-ir-spec — G-1 bis G-7 sind dort ohnehin als Anforderungsabschnitt vorgesehen (jdl-effect-rows-und-throw §14). V gehört in dieselbe Runde, weil es dasselbe Artefaktformat betrifft.


13. Nicht-Ziele

Diese Notiz liefert keine fertige Syntax. Alle Formen sind illustrativ und tragen // spekulativ.

Sie führt keinen Runtime-Handler-Lookup ein und macht keine Aussage, die A-6 oder L-2 aufweicht.

Sie ist keine Spezifikation. Sie ersetzt weder jdl-sprachreferenz noch jdl-effect-rows-und-throw und hat gegenüber beiden keinen Vorrang.

Sie entscheidet keine der in §11 als offen geführten Fragen. Wo die Quellen sich widersprechen (§4.4, §4.5, §8.1), steht der Widerspruch — nicht seine Auflösung.


Änderungsprotokoll

v0.2.0 — Die Meta-Phase ist von einem Vorschlag zu einem Arbeitskonsens geworden. §8.3 hält die Phase vor der Row fest, §8.4 die Zweiwertigkeit mit Runtime als Default und streicht den Bootstrap-Kandidaten mit Begründung, §8.5 die Aufrufrichtungen samt der einseitigen Abschwächung, die daraus folgt. §8.6 fasst „was ist erfüllbar" und „was darf Meta aufrufen" zu einer Frage zusammen und markiert Alloc als Vorfahrt. §8.8 trennt Form von Umfang und hält fest, dass die Umfangsgrenze im Evaluator liegt, nicht in der Sprache. §8.9 führt die Phasenvariable p als bewusst vertagt mit Reihenfolge. §8.10 benennt die Artefaktfolge: Für Meta-Aufrufe über Modulgrenzen braucht der Compiler Rümpfe, was der Begründung von 07 §7.4 zuwiderläuft. Offene Punkte neu sortiert: P, Q, S, T geklärt, R weitgehend, U, V, W neu.

v0.1.1 — §8.6 ergänzt: Wird die Phase vor die Row gezogen, ist sie eine eigene Dimension des Funktionstyps. Vier direkte Konsequenzen aufgenommen — Default-Phase, Typidentität, Higher-Order-Wiederverwendung, erlaubte Aufrufrichtungen — alle als offen markiert. Offene Punkte P bis T ergänzt; R (reine Hilfsfunktionen in beiden Phasen) ist der einzige davon, der nicht warten kann. Nachziehliste um 07 §7.4 erweitert (Phase in Modul-Metadaten).

v0.1.0 — Erstfassung. Hält den Konsens einer Gesprächsrunde fest: service als TypeKind statt Top-Level-Konstrukt, Wertstatus als tragendes Argument gegen die Blueprint-Lösung, Rollen als Phantom-Tags über geschlossene Domains, statische Materialisierung der Handler-Auswahl. Zwei zwischenzeitliche Argumente gegen die Blueprint-Lösung sind zurückgezogen (§1.1). Neu gegenüber beiden Gesprächsvorlagen: der Vorschlag, die Meta-Phase vor die Row zu ziehen (§8.3), und die daraus folgende Umformulierung der Stage-Frage (§8.4).