Zum Inhalt

Notiz: Operatoren — Arbeitsstand

Quelle: Arbeitsgespräch 2026-10-11; Architekturreview durch ChatGPT (GPT-6) am selben Tag, eingearbeitet mit Korrekturen Status: Arbeitsstand, noch nicht in die Specs übernommen Markierungen: [D] Entscheidung · [A] ableitbar · [O] offen

Berührt: 02-typsystem §2, jdl-sprachreferenz §11.5, 07-cast-effekte-refinements §1, note_two §6


1. Grundbegriffe

Ein Operator hat zwei Hälften, die getrennt behandelt werden:

  • Schreibweise: wo das Symbol steht und wie stark es klebt. Das ist Parser-Arbeit.
  • Bedeutung: was der Operator für einen Typ tut. Das ist Typprüfung.

Es gibt zwei Arten von Operatoren:

  • Rechenoperatoren (+, ==, <): Die Bedeutung kommt vom Typ.
  • Strukturoperatoren (=, and/or, :>, &, ., ::): Die Bedeutung kommt von der Sprache. and/or sind Kontrollfluss, weil die rechte Seite nicht immer ausgewertet wird.

Operatoren existieren zur Laufzeit nicht. Nach der Expansion ist a - b ein gewöhnlicher Aufruf. Die VM sieht nie einen Operator. [A]


2. Wer Operatoren einführen darf

Plugins dürfen neue Operatoren einführen. [D] Begründung: Nephrit ist selbst ein (privilegiertes) Plugin und muss seine Operatoren ohnehin in JDL definieren. Den Mechanismus dem Benutzer zu verbieten, wäre inkonsequent. Vorbild ist das Syntax-Plugin der Do-Notation mit <-.

Ein Symbol gehört genau einem Paket. Alle Stellungen eines Symbols (Präfix, Infix, Postfix) müssen aus demselben Paket stammen. Ein Plugin darf einem fremden Symbol keine neue Stellung hinzufügen. [D] Das ist die Orphan-Regel, angewandt auf Operatoren.


3. Versiegelung

Drei Sorten von Operatoren: [D]

Sorte Bedeutung Beispiele
Ganz versiegelt Die Sprache bestimmt sie, kein Typ redet mit =, and/or, :>, &
Vertrag versiegelt Symbol fest an ein Protocol gebunden, Ausführung liefert der Typ ==, <, ->
Frei Typ bestimmt die Bedeutung frei Rechenoperatoren, selbst erfundene Operatoren

Kriterium für „ganz versiegelt“: Die Bedeutung hängt nicht vom Typ der Werte ab, sondern handelt von Bindungen, Kontrollfluss, Typen oder Phasen. [A]

Grenze bei „Vertrag versiegelt“: Der Compiler prüft die Form (z. B. „liefert bool“), nicht ob ein == tatsächlich Gleichheit ist. Das Siegel sichert die Bindung, nicht die Ehrlichkeit. [A]

Grenze bei „frei“: Freiheit erlaubt auch Unsinn. Beispiel aus 02-typsystem §2: Das Add für UserStats mittelt avgAge, damit ist (a + b) + c ungleich a + (b + c). Das ist der Preis der Freiheit. [A]

Trusted Core: Die Definition versiegelter Operatoren lebt in Nephrit (Ring 1). Das Siegel selbst setzt ein Mechanismus durch, an den JDL-Code nicht herankommt (CompilerDB/Constitution, letztlich Zig). Wer versiegeln darf, ergibt sich aus der Herkunft (Coherence-Paket jade::/jdl::), nicht aus einem Feld im Blueprint. [D]


4. Rangfolge

4.1 Teilordnung statt Zahlenleiter [D]

Klebkraft wird nicht als globale Zahlenleiter festgelegt, sondern als einzelne Beziehungen („stärker als“, „schwächer als“). Wo zwischen zwei Operatoren keine Beziehung besteht, muss geklammert werden. Ein neues Plugin kann bestehenden Code so nie umdeuten, nur zum Klammern zwingen. Suchwort: partial precedence

4.2 Gruppen [D]

Ein Operator trägt seine Klebkraft nicht selbst, sondern gehört einer Gruppe an. Die Gruppe trägt Gruppierung und Beziehungen.

  • Gleiche Gruppe heißt gleich stark; die Gruppierung der Gruppe entscheidet (a - b + c ist (a - b) + c).
  • Die Gruppierung gehört an die Gruppe, nicht an den Operator.
  • Benennung: strongerThan / weakerThan.
  • Eine Gruppe enthält nur Operatoren einer Stellung. Sonst trüge sie eine Gruppierung, die für Präfix- und Postfix-Mitglieder bedeutungslos ist. [D] (aus dem Review übernommen)

4.2a Wo Rangfolge überhaupt gebraucht wird [A]

Vergleiche zwischen Gruppen braucht es nur an vier Stellen:

  • Infix gegen Infix (a + b * c)
  • Präfix gegen Infix (-a * b)
  • Infix gegen Postfix (a + b!)
  • Präfix gegen Postfix (-a!)

Präfix gegen Präfix ist nie mehrdeutig; Ketten wie - !a lösen sich von innen nach außen. Postfix gegen Postfix ebenso.

Zwei Operatoren derselben Gruppe mit grouping = .none nebeneinander sind ein Fehler (a == b == c). Verkettete Vergleiche im Python-Stil gibt es damit nicht, es sei denn, sie werden als eigene Expansion eingeführt.

Suchwort: precedence groups (Swift)

4.3 Gerüst und Besitz [D]

  • Nephrits Gruppen bilden das versiegelte Gerüst, an dem sich alle anderen einordnen.
  • Beziehungen einer Gruppe dürfen nur auf Gruppen des Gerüsts und auf Gruppen des eigenen Pakets zeigen, nie auf Gruppen anderer Plugins.
  • Benutzer-Operatoren treten Nephrits Gruppen nicht bei. Eine Gruppe gehört einem Paket, ihre Mitglieder kommen aus diesem Paket.

Folgen: [A] - Fremde Plugins müssen nichts voneinander wissen. - Zwei Benutzer-Operatoren in derselben Lücke des Gerüsts haben keine Beziehung, also wird geklammert. - Ein Kreis entsteht nur, wenn eine Gruppe sich selbst widerspricht („stärker als Multiplikation, schwächer als Addition“). Das ist lokal prüfbar. - Ein neues Plugin kann bestehenden Code nie umdeuten, nur zum Klammern zwingen. Eine neue Version desselben Plugins kann es sehr wohl: Stellt sie die Beziehungen ihrer Gruppe um, wird a + b <+> c still anders gruppiert.

Operatorprofile und Gruppenbeziehungen gehören deshalb zur Schnittstelle des Plugins und müssen in dessen Fingerprint eingehen; die CompilerDB führt sie als ausdrückliche Abhängigkeiten. [D] (aus dem Review übernommen)


5. Blueprint, Engine, Descriptor

5.1 Identität eines Operators [D]

Die Identität eines Operators ist Symbol plus Stellung (Variante A). Präfix-Minus und Infix-Minus sind zwei getrennte Operatoren mit je eigenem Profil.

5.2 Blueprints

type PrecedenceGroup : blueprint {                // spekulativ
    var grouping: enum { left, right, none } = .none
    var strongerThan: [PrecedenceGroup] = []
    var weakerThan: [PrecedenceGroup] = []
}

type Operator : blueprint {                       // spekulativ
    var symbol: str
    var position: enum { prefix, infix, postfix }
    var group: PrecedenceGroup
    var desugar: Desugar
}
  • PrecedenceGroup: alle Felder mit Default; eine Gruppe ohne Angaben zwingt überall zum Klammern.
  • Operator: Symbol, Stellung, Gruppe und Entzuckerungsziel sind Pflicht.

Beispiel Gerüst (Nephrit) und Benutzer-Operator:

type Addition : PrecedenceGroup & {               // spekulativ
    grouping = .left
}

type Multiplication : PrecedenceGroup & {         // spekulativ
    grouping = .left
    strongerThan = [Addition]
}

type Minus : Operator & {                         // spekulativ
    symbol = "-"
    position = .infix
    group = Addition
    desugar = .method(Sub.sub)
}

type Combining : PrecedenceGroup & {              // spekulativ
    grouping = .left
    weakerThan = [Addition]
}

type Combine : Operator & {                       // spekulativ
    symbol = "<+>"
    position = .infix
    group = Combining
    desugar = .method(Combinable.combine)
}

Die Schreibweise .method(Sub.sub) für eine Methode als Wert ist Platzhalter. [O]

5.3 Entzuckerungsziel [D]

Desugar hat zwei Varianten:

  • Methode: als sichtbare Daten im Descriptor. Nötig, damit die Siegelprüfung für „Vertrag versiegelt“ die Bindung ablesen kann, ohne Code zu verstehen.
  • Expansion: als Code, für alles, was mehr als einen Aufruf braucht (and → if; abgeleitete Operatoren wie !=, <=).

5.4 Engine [A]

  • Lebt nur in der Meta-Phase: nur describe, kein instantiate.
  • Prüft lokal am einzelnen Profil: Symbol besteht nur aus Symbolzeichen; Entzuckerungsziel passt zur Stellung (Infix: zwei Werte, Präfix/Postfix: einer); Gruppe stammt aus dem eigenen Paket; alle Mitglieder einer Gruppe haben dieselbe Stellung; eine Gruppierung ist nur bei Infix-Gruppen gesetzt; Beziehungen zeigen nur auf erlaubte Gruppen; keine Selbstwidersprüche.
  • Fehlschlag über Throw in der Meta-Row.

Die Form der Engine selbst (Rolle engine oder provide auf leerem Struct) hängt an der allgemeinen Engine-Frage. [O]

5.5 Globale Prüfung im Zig-Kern [A]

Einmal pro Kontext, als Teil der Auflösung der Operator-Ketten:

  • Doppelte Operatoren (gleiches Symbol, gleiche Stellung).
  • Symbolbesitz über die Herkunft, die die CompilerDB vermerkt.
  • Infix und Postfix mit demselben Symbol sind verboten (a ! b wäre mehrdeutig).

Die Kreisprüfung ist durch das Gerüst lokal geworden (§4.3); global bleibt nur, was mehrere Profile betrifft.

5.6 Descriptor [A]

type OperatorDescriptor : record {                // spekulativ
    symbol: str
    position: enum { prefix, infix, postfix }
    group: GroupId
    desugar: enum { method: MethodId, expansion: ExpansionId }
}
  • Fertig aufgelöst, alle Defaults ausgeschrieben.
  • Kopf (Herkunftspaket, daraus abgeleitet: versiegelt) schreibt die CompilerDB, nicht die Engine.
  • Kommt nie ins Artefakt; die Selektion lässt Operator-Descriptoren weg.

Verweise auf Operatoren laufen über eine Identität aus Besitzerpaket, Symbol und Stellung, nicht über den Profilnamen. [D]


6. Rows und Operatoren

  • Ein Operator, der zu einer Methode entzuckert, übernimmt deren Row. Was sie braucht, steht in der Row der aufrufenden Funktion; wer werfen kann, braucht try. Operatoren sind damit nicht undurchsichtiger als Funktionsaufrufe. [A]
  • Braucht ein + eine abweichende Allokation (z. B. Scratch Arena), steht Storage in der Row der Implementierung. Standard-Allokation steht nirgends in der Row; das gilt für Funktionen genauso und ist kein Operator-Problem. [D]
  • Die Phase gehört zum Pfeil: Die Methode hinter einem Operator ist Laufzeit-Code. Ob ein bestimmtes + auch in der Meta-Phase benutzbar ist, entscheidet N-15 vollständig: deterministisch, total, kein Service in der Row, Throw erlaubt. Die Row allein reicht nicht, Totalität steht nicht in ihr. Operatoren bekommen keine Sonderregel, die Phasengrenze verschwindet durch das Entzuckern aber auch nicht. Es braucht keine getrennte Meta-Methode. [A] (korrigiert nach Review; frühere Fassung „ergibt sich aus der Row“ war zu knapp) Prüffall: ein + auf i32, das bei Überlauf einen Trap auslöst — ist das im Sinne von N-15 total?
  • Operatoren sollen keine „fancy“ Dinge wie Netz- oder Dateizugriff machen; das ist missbrauchte Logik, nicht nur versteckter Programmfluss. Die Beschränkung sitzt im Row-Platz des Protocols: Nephrit begrenzt ihn für Rechen-Protocols, eigene Protocols begrenzt ihr Autor. [D]
  • Expansionen laufen in der Meta-Phase und brauchen nur den SyntaxBuilder (eventuell Lesezugriff auf die CompilerDB); nach außen greifen können sie nicht. Der erzeugte Code wird wie jeder Code geprüft und bleibt in der Row sichtbar, ist aber nicht durch ein Protocol beschränkt. [A]

6a. Expansionen: Sicherheitsvertrag [A]

  • Kein Einschleusen: Eine Expansion erzeugt gewöhnliche Syntax, die danach wieder durch die regulären Abfragen geprüft wird (note_two §7). Kein zweiter Compiler.
  • Hygiene: Der SyntaxBuilder baut Knoten, keinen Text. Von einer Expansion erzeugte Namen sind eigene Identitäten und können Variablen des Benutzers nicht versehentlich einfangen. Suchwort: hygienic macros
  • Terminierung: Jede Expansion ist für sich total, die Kette nicht (⊕ erzeugt ⊗, ⊗ erzeugt ⊕). Die Zyklenerkennung der CompilerDB greift nicht, weil jede Runde neue Knoten erzeugt. Lösung: eine Tiefengrenze mit klarer Fehlermeldung im Treiber des Expansionssystems. Betrifft alle Syntax-Plugins, nicht nur Operatoren. (aus dem Review)

7. Parsing

  • Die Auflösung der Rangfolge geschieht in zwei Schritten: erst flache Ketten erkennen („Wert, Symbol, Wert, …“), dann die Rangfolge anhand der Descriptoren auflösen. Der Zig-Parser bleibt dadurch unabhängig von Plugins. Suchwort: fixity resolution [A]
  • Pratt allein passt schlecht zu einer Teilordnung, weil Pratt Zahlen braucht. [A]

8. Offene Punkte

Nr. Punkt Betrifft
O-1 Identität von Gruppen. Gruppen haben kein Symbol; „Paket, Symbol, Stellung“ passt nicht. Vorschlag (übereinstimmend mit Review): Paket plus Name als Identität für Verweise; daneben ein Inhalts-Hash für Invalidierung und Cache. CompilerDB
O-2 Verweise auf Identität gegen Verweise auf Inhalt. Gegenseitige Verweise über Inhalts-Hashes bilden Kreise (A zeigt auf B, B auf A). Die CompilerDB braucht zwei Arten von Verweis. Betrifft auch gegenseitig verweisende Typen. Vorschlag aus dem Review: Verweise über stabile Identitäten, jeder Descriptor bekommt nur einen lokalen Hash plus ausdrückliche Abhängigkeitskanten; Kreise gemeinsam zu hashen erst, wenn sich ein transitiver Hash als nötig erweist. Berührt die Entscheidung „Verweise laufen über den Hash“. Suchwort: content addressing cycles Zig-Kern
O-3 Lexer-Regel. Ist eine zusammenhängende Folge von Symbolzeichen immer genau ein Token? Dann ist a<-1 stets <-, und a < -1 braucht Leerzeichen. Hält den Lexer unabhängig von Plugins. Suchwort: maximal munch Zig-Kern
O-4 Operatorseiten auf zwei Achsen (übernommen aus dem Review, ersetzt die frühere Viererliste): Rolle (Wert, Typ, neuer Name) und Auswertung (sofort, aufgeschoben); aufgeschoben ergibt nur bei Wert-Seiten Sinn. Regel für sofort ausgewertete Werte: genau einmal, von links nach rechts, keine Verdopplung durch die Expansion; umgesetzt über temporäre Bindungen vor der Expansion. Rohe Syntax nur für aufgeschobene Seiten. Die Garantie muss das Expansionssystem durchsetzen. Offen: ob die Regel so beschlossen wird. Expansionssystem
O-5 Obergrenze für Row-Plätze in Protocols („höchstens Storage, Throw“). Gewollt, Schreibweise fehlt; keine operatoreigene Syntax, sondern dieselbe Form wie bei begrenzten Funktionsfeldern in Kontexten. Zusätzlich zu klären: Begrenzt die Obergrenze bei Throw[E] auch, welches E erlaubt ist? Sprache
O-6 Form der Engine (Rolle engine oder provide auf leerem Struct). Das Review bevorzugt eine Protocol-Implementierung; Elias' bisherige Tendenz ist eine eigene Rolle engine. Nicht aus dem Operatorensystem heraus entscheiden. allgemeine Engine-Frage
O-7 Schreibweise einer Methode als Wert (.method(Sub.sub)). Sprache
O-8 Zuordnung im Einzelnen: welcher Operator in welche Sorte fällt. Kriterium ist die Verantwortung, nicht das Symbol. Für ..: Hat es eine eigene Rolle in Mustern, Slices oder anderen Grammatikstellen, nicht freigeben; ist es nur Bequemlichkeit für einen Bereichswert, reicht „Vertrag versiegelt“. Nephrit
O-9 Zwei Plugins, ein Symbol. Definieren zwei Bibliotheken dasselbe Symbol, sind sie im selben Kontext nicht gemeinsam benutzbar (Fehler beim Bauen, keine stille Wahl). Braucht es eine Ausweichmöglichkeit, etwa ein Symbol beim Einbinden eines Plugins in den Kontext umzubenennen? Modulweise Aktivierung (Vorschlag aus dem Review) widerspricht der Entscheidung, dass Syntaxerweiterungen den ganzen Kontext betreffen, und wird nicht übernommen. Kontext/Plugin-System
O-10 Tiefengrenze für Expansionen (§6a): Höhe, Fehlermeldung, ob einstellbar. Expansionssystem

9. Streichliste für ältere Dokumente

Umformuliert: - 02-typsystem §2 und jdl-sprachreferenz §11.5: Die Bindung über ein Meta-Record am Protocol (protocol <{ operator: "+" }> Add) wird durch Operator-Profile mit Methoden-Entzuckerung ersetzt. - „Unäre und binäre Operatoren mit demselben Symbol werden über die Stelligkeit unterschieden“: wird zu zwei getrennten Operatoren (Symbol plus Stellung); die Stelligkeit wird zur lokalen Prüfung der Engine. - „!= wird automatisch als Negation von == abgeleitet“, derivedOps: werden zu versiegelten Expansionen in Nephrit. - jade-vision „Operator-Bindings über das Typsystem“: bleibt im Kern gültig, Mechanismus wie oben.

Ersatzlos gestrichen: - :> Operator("symbol") als Suffix-Form (Zusammenfassung Teil 2 in 02-typsystem).