Semantische Analyse
Läuft nach dem Parsen bei der Standardkompilierung und bei --print-types;
reine Syntaxaktionen überspringen sie. Fehler stoppen den betroffenen Batch vor
LLVM; Diagnosen behalten ihre Makro-/Include-Herkunft, und spätere Eingaben
werden weiterhin diagnostiziert.
Modul- und Aufrufprüfungen
- Eine Moduldeklaration ist erforderlich und eindeutig; Funktionsstelligkeiten 0..255; keine doppelten Definitionen; jeder Export existiert und ist genau einmal aufgeführt. Quotierte/Unicode-Namen behalten ihre exakte Identität.
- Jede Funktion wird geprüft, auch ungenutzter und unerreichbarer Code.
- Aufrufe werden innerhalb des Batches über Modul/Name/Stelligkeit aufgelöst. Remote-Aufrufe (auch selbstqualifizierte) erfordern Exporte. Fehlende/private Aufgerufene und doppelte Module sind Fehler.
fun F/Amuss eine Funktion des Moduls benennen (function F/A undefined);fun M:F/Aund Aufrufe von Werten (F(Args)) werden nicht zur Kompilierzeit aufgelöst (Funs). Jeder unterschiedliche fun-Wert erhält nach der Bindungsanalyse einen Eintrag inModule::funs.- Selbst-, wechselseitige und modulübergreifende Rekursion werden akzeptiert. Der Aufrufgraph wird in starke Zusammenhangskomponenten (iterativer Tarjan) in der Reihenfolge Aufgerufener vor Aufrufer zerlegt; eine Komponente ist rekursiv, wenn sie mehrere Mitglieder hat oder ein Mitglied sich selbst aufruft.
Bindungen
Jede Bindung hat eine funktionsrelative Identität clause[N].local[M].
Vorkommen sind Definitionen, Lesezugriffe oder Prüfungen auf exakte Gleichheit,
markiert mit Kopf-/Guard-/Rumpfkontext. Die Analyse ist deterministisch.
- Jede Klausel beginnt mit einer leeren Umgebung. Kopfkandidaten sammeln vorläufige Definitionen; Guards lesen sie; der Rumpf sieht sie nur bei Erfolg.
_bindet nichts;_Nameist eine gewöhnliche Variable. Ein wiederholter Name ist eine Bedingung auf exakte Gleichheit.- Matches im Rumpf besuchen die rechte Seite vor der linken; Ketten werden von
rechts nach links ausgewertet. Ein geklammertes Muster
P1 = P2ist ein Alias, keine Sequenz. - Geschwisterausdrücke lesen dieselben eingehenden Namen; ihre Definitionen
werden nach dem gesamten Ausdruck exportiert.
{X = 1, X}ist ein Lesezugriff auf eine ungebundene Variable;{X = 4, X = 3}ist zulässig und schlägt zur Laufzeit fehl. - Definitionen auf der rechten Seite von
andalso/orelseoder innerhalb voncatch Exprsind danach unsicher (OTPvtunsafe); vor einemcatchgebundene Namen bleiben nutzbar. Jeder innerhalb einestrygebundene Name ist danach unsicher;of-Klauseln sehen die Namen des Rumpfes, catch-Klauseln sehen sie als unsicher, und der after-Rumpf sieht jeden zuvor im try gebundenen Namen als unsicher. Die Stack-Variable einer catch-Klausel muss neu sein (OTPstacktrace_bound), und ihr Guard darf sie nicht lesen (stacktrace_guard). Einmaybeexportiert nichts: jedes?=bindet für die folgenden Rumpfausdrücke,else-Klauseln sehen die Namen des Rumpfes als unsicher, und jeder darin gebundene Name ist danach unsicher. Guards veröffentlichen nie Bindungen; Matches in Guards sind Fehler, selbst wenn sie unerreichbar sind. - Map-Schlüssel lesen nur eingehende Bindungen; Binary-Größen lesen zusätzlich frühere Segmente desselben Binarys (siehe Muster).
- Der Prüfausdruck (scrutinee) eines
casebindet im umschließenden Gültigkeitsbereich. Jede Klausel beginnt in diesem Bereich; ihre Musterdefinitionen sind bis zum Guard vorläufig, der nur liest. Von jeder Klausel gebundene Namen werden mit einer Identität exportiert (spätere Klauseln verwenden die Identität wieder, die eine frühere Klausel dem Namen gab); nur von einigen Klauseln gebundene oder in irgendeiner unsichere Namen sind danach unsicher. Exporte werden konservativ zusammengeführt wie bei OTPserl_lint(icrt_export); OTPs Warnung, wenn ein späteres Muster einen exportierten Namen matcht, wird nicht ausgegeben.if-Klauseln folgen denselben Regeln mit einem Guard und ohne Muster. begin/endist eine Sequenz im umschließenden Gültigkeitsbereich.- Ungebundene, unsichere und Wildcard-Lesezugriffe sind Fehler mit
Ortsangabe. Meldungen behalten den Wortlaut des Compilers (
unbound variable X,unsafe variable X); der Bindungskorpus prüft jede gegen OTPs Klasseunbound_var/unsafe_var. - Eine comprehension wertet jeden Qualifier der Reihe nach im Gültigkeitsbereich aus, den die vorherigen hinterlassen. Generatormuster binden neue Namen, die äußere verdecken (Lesezugriffe innerhalb des Musters bevorzugen Namen, die es bereits gebunden hat); eine Zip-Gruppe bindet alle ihre Muster gemeinsam. Templates lesen wie Geschwister. Der Gültigkeitsbereich nach der comprehension ist derjenige davor: ihre Namen sind dort ungebunden.
- Die Klauseln eines anonymen fun beginnen jeweils im Gültigkeitsbereich am
fun: Kopfnamen sind neu und verdecken äußere, Guards lesen sie, und nichts
darin Gebundenes ist nach dem fun sichtbar. Jede äußere Definition, die darin
gelesen wird, wird als Capture erfasst (
Function::captures, Definitionsreihenfolge). Der Name eines benannten fun ist eine weitere Definition, mit der jede Klausel beginnt (Function::fun_names); er wird nie erfasst. Die Variablen vonfun M:F/Asind Lesezugriffe des fun-Ausdrucks (Function::fun_operands).
Durchläufe sind iterativ mit einem Modulbudget von 1.000.000 Arbeitseinheiten. Erschöpfung oder ein beliebiger semantischer Fehler leert die Bindungs- und Normalisierungstabellen des Moduls.
Typen und Spezifikationen
Ein privater Typgraph stellt alle geparsten Typformen unabhängig vom
Runtime-Layout dar: Singletons, Bereiche, Container, Rollen von Map-Feldern,
Funktionsprodukte und unaufgelöste Anwendungen. Unions werden abgeflacht und
dedupliziert; term() ist das obere und none() das untere Element.
Standardwerte: 16.384 Knoten, 16 Union-Mitglieder, 100.000 Arbeitsschritte pro
Übersetzung. Erschöpfung weitet auf term() mit sichtbarem Flag auf und engt
nie eine Repräsentation ein.
Deklarierte Metadaten (-type, -opaque, -nominal, -export_type, -spec,
-callback, -optional_callbacks) folgen der OTP-Typespec-Referenz und dem
festgelegten Verhalten von erl_lint/erl_internal/erl_types:
- Lokale Aliase dürfen eingebaute Typnamen verdecken. Remote-Typen des Batches
müssen exportiert sein. Fehlende Typen, Duplikate, fehlerhafte Metadaten,
Typvariablen mit nur einem Vorkommen, ungültige Schranken und Specs für
fehlende Funktionen sind Fehler. Nicht verfügbare externe Typen erzeugen eine
Warnung und werden zu
term(). - Jeder Alias und jede Überladung hat einen eigenen Variablenbereich; wiederholte formale Parameter folgen OTPs Substitution durch das letzte Argument.
- Rekursive Aliase bleiben endliche benannte Referenzen; opake Rümpfe werden nur in ihrem Modul expandiert; nominale Namen bleiben überall erhalten, und nur die Spezifikationsprüfung liest die Definition eines nominalen Typs, innerhalb seines Moduls.
- Die Konstantenauswertung ist exakt, begrenzt auf 10.000 Dezimalstellen pro Wert.
Inferenz
Die Inferenz ist von deklarierten Typen getrennt und vertraut Specs nie.
- Exportierte Eingaben und Eingaben von Funktionen, die
fun F/Abenennt, sind beliebige Terme. Eine Funktion, die nur über direkte Aufrufe des Batches betreten wird (weder exportiert noch von einem fun benannt), nimmt die Vereinigung (join) der Argumentfakten ihrer Aufrufstellen als Eingaben (step 58F,semantic/types/inference_inputs), gematcht durch die Kopfmuster jeder Klausel; rekursive Aufrufe zählen mit. Eingaben beginnen beinone()(eine Funktion, die nichts aufruft, läuft nie und inferiertnone()) und wachsen über Durchläufe des Batches: 8 Durchläufe vereinigen, spätere weiten auf wie rekursive Ergebnisse; Eingaben, die sich nach 16 Durchläufen nicht stabilisiert haben, werden zuterm()(als aufgeweitet gemeldet). - Literale haben exakte Fakten (step 58B):
Ganzzahlen jeder Größe und Zeichen sind Singletons, Atome Singleton-Atome,
Gleitkommazahlen
float(), Strings nichtleere Listen ihrer Zeichen ([]für""), und-/+einer literalen Zahl behält deren Fakt. Tupel behalten die Fakten ihrer Elemente; mit konstanten Schlüsseln gebaute Maps (ein Fakt eines einzigen Werts: eine Ganzzahl, ein Atom,[]oder ein Tupel oder eine Map daraus) behalten den Wertfakt jedes Schlüssels, wobei ein zweimal angegebener Schlüssel seinen letzten Wert behält, und jeder andere Schlüssel ergibtmap(). Eine bitstring-Konstruktion zählt ihre Größe: literale, größenangegebene und UTF-kodierte literale Zeichen exakt, ein UTF-Segment eines anderen Werts nach seiner Kodierung (UTF-8: 8 bis 32 Bits nach Bytes), und einbinary/bitstring-Segment nach dem Fakt seines Werts oder einem beliebigen Vielfachen seiner Einheit (<<1, Rest/binary>>istnonempty_binary()). Ein Operand, der nie einen Wert erzeugt (none()), macht die Konstruktion zunone(). - Operatoren und Builtins (step 58C,
semantic/types/inference_operators) berechnen ihr Ergebnis aus den Fakten ihrer Operanden. Ganzzahl-Singletons werden exakt gefaltet bis zu 4.096 Bits pro Operand und Ergebnis (Bignums eingeschlossen; ein größeres istinteger()); andere Ganzzahlen verwenden Intervallarithmetik für+,-,*nichtnegativer Bereiche,bandmit einem nichtnegativen Operanden (X band 15ist0..15),remdurch einen beschränkten Divisor (X rem 10ist-9..9),bnot, unäres-undabs/1; ein Gleitkommaoperand ergibtfloat(), ein unbekannternumber(), und/ist immerfloat(). Vergleiche werden gefaltet, wenn sich die Werte der Operanden nur auf eine Weise vergleichen lassen (Singleton-Atome oder -Ganzzahlen, disjunkte Ganzzahlbereiche, Zahlen gegen Nicht-Zahlen), und sind sonstboolean();and/or/xor/notundandalso/orelsekombinieren die Wahrheitswerte, die ihre Operanden haben können (der rechte Operand vonandalsoist das Ergebnis, wenn der linketrueist). Eine Tabelle gibt jedem Bridge-Builtin sein Ergebnis:pid()fürself/0undspawn/1,3,reference(),port(),non_neg_integer()für Größen,string(),nonempty_string(),binary(),list(),tuple(),atom(),boolean()für Typtests,true/okfür Builtins mit Seiteneffekten, die Nachricht für!undsend/2;trunc/1und verwandte Funktionen behalten Ganzzahloperanden; unbekannte Builtins sindterm(). Eine Operation, die immer eine Ausnahme auslöst (1 + a,1 div 0,error/1,exit/1,throw/1,halt/0,1), istnone(), underlang:raise/3liefert nurbadarg. Das Falten ändert nie den erzeugten Code: Überläufe und Fehlschläge bleiben Ergebnisse zur Laufzeit. - Ein Rumpf, dessen Ausdruck nie abschließt (
none()), istnone(); ebenso ein Aufruf mit einem solchen Argument. Pfade mit Ausnahmen tragen nichts zu einer Vereinigung bei, daher inferiert eine Funktion, die immer eine Ausnahme auslöst,none(). - Identitäts-/Projektionsfunktionen behalten exakte Argumentrelationen, weitergegeben durch verschachtelte lokale und Remote-Aufrufe mit frischen Variablen pro Aufruf.
- Container (step 58D,
semantic/types/inference_containers): eine Liste[E1, ..., En | T]vereinigt ihre Elemente vor dem Fakt des Tails (eine echte Liste, wenn der Tail eine ist,nonempty_improper_list(H, T), wenn er keine Liste ist,term(), wenn er unbekannt ist);++,--,hd/1,tl/1,element/2,setelement/3,tuple_to_list/1undmap_get/2lesen und bauen Elementfakten Mitglied für Mitglied einer Union neu auf, wobei ein Mitglied, das eine Ausnahme auslösen würde, nichts beiträgt. Ein Map-Update behält exakte Schlüssel (:=eines fehlenden Schlüssels verwirft dieses Mitglied) und ergibtmap()bei einer unbekannten Map. Tupel-records sind Tupel: die Konstruktion füllt Standardwerte (undefinedohne einen), Zugriff und Update lesen und setzen das Feld der passenden Tupel, und#r.fist der Index des Feldes. Eine List-comprehension ist eine möglicherweise leere Liste der Fakten ihrer Templates, eine Binary-comprehension eine beliebige Anzahl von Kopien der Größe ihres Templates, eine Map-comprehensionmap(). - Funs (step 58E,
semantic/types/inference_funs):fun F/Aist ein fun seiner Stelligkeit, der das inferierte Ergebnis der Funktion liefert (das eines Builtins, oder bei variabler Stelligkeitfun()),fun M:F/Aeiner, derterm()liefert, und ein anonymer fun einer, der die vereinigten Ergebnisse seiner Klauseln liefert, erfasste Werte eingeschlossen. Ein Aufruf eines Werts vereinigt die Ergebnisse seiner funs dieser Stelligkeit, die seine Argumente auswählen (step 58L;term()für einen unbekannten fun,none(), wenn kein Mitglied so aufgerufen werden kann). Ein anonymer fun, der als Ganzes an eine Variable gebunden und über sie aufgerufen wird, wird für diesen Aufruf erneut ausgewertet, wobei seine Muster die Fakten der Argumente matchen, höchstens 4 solche Aufrufe tief, und die Fakten seiner ersten Auswertung werden danach wiederhergestellt (Double = fun(Y) -> Y * 2 end, Double(3)ist 6).apply/2,3und dynamische Aufrufe bleibenterm(). Dafun F/Aeine später inferierte Funktion benennen kann, wiederholen sich die Durchläufe über den Batch außerdem, bis jeder solche fun das endgültige Ergebnis seiner Funktion gelesen hat; andernfalls gibt ein letzter Durchlauf ihnenterm()-Ergebnisse. - Zuweisungen ganzer Werte im Rumpf und Aliase kopieren den Fakt der rechten
Seite (
Y = 42, Z = Y, id(Z)inferiert 42); Tupel-, Listen-, Map- und Tupel-record-Muster geben ihren Variablen die Fakten der Teile, die sie matchen, in Matches im Rumpf,case-Klauseln (vom Prüfausdruck), denof-Klauseln einestry(vom Wert seines Rumpfes) und Generatoren (von den Elementen ihrer Eingabe bzw. Map-Schlüsseln und -Werten). Nicht bewiesene Werte bleibenterm()ohne Relationen. - Einengung (narrowing) (step 58G,
semantic/types/inference_narrowing,inference_scopes,meet): innerhalb einer Funktions-,case-,receive- oder fun-Klausel bildet jedes Muster den Schnitt (meet) mit dem Wert, den es matcht (ein Literal, eine Tupel-, Listen-, Tupel-record-, Map- oder bitstring-Form; der Fakt einer gebundenen Variable), einecase-Prüfvariable wird damit eingeengt, und der Guard engt die Variablen ein, die er testet. Typtests (is_atom/1aufatom(),is_boolean/1,is_integer/1,is_float/1,is_number/1,is_binary/1,is_bitstring/1,is_list/1aufmaybe_improper_list(),is_tuple/1,is_map/1,is_function/1,2,is_pid/1,is_port/1,is_reference/1,is_record/2,3auf das Tupel des records,is_map_key/2seine Map aufmap(), und die alten Guard-Namen) bilden den Schnitt mit dem Fakt ihres Arguments; Vergleiche mit Ganzzahlkonstanten (<,=<,>,>=,==,=:=, auf beiden Seiten, gefaltete Konstanten eingeschlossen) engen einen bereits als Ganzzahl bewiesenen Wert auf einen Bereich ein, wobei sich die Schranken eines Tests ansammeln (10 >= X, X >= 0ist0..10); zwei verglichene bewiesene Ganzzahlen engen einander über ihre Schranken ein (X > YmitYin0..5machtXmindestens 1), und/=,=/=mit einer Konstante verschieben eine ihr gleiche Bereichsschranke nach innen (step 58H1). Ein Wert, der eine Gleitkommazahl oder ein anderer Term sein kann, wird nicht eingeengt. Eine Konjunktion wendet jeden Test der Reihe nach an, eine Disjunktion vereinigt, was jede Alternative beweist,notund falsche Tests beweisen nichts. Eine Klausel nach einer, deren Muster alle einfache Variablen sind und deren gesamter Guard ein einzelner Typtest war, sieht diesen Wert ohne die getestete Kategorie; nach einem einzelnen Vergleich mit einer Ganzzahlkonstante sieht derselbe Wert (eine einfache Variable dieser Klausel), als Ganzzahl bewiesen, den Vergleich als falsch (f(N) when N >= 0 -> ...; f(N) when is_integer(N) -> ...: die zweite Klausel siehtneg_integer()). Der rechte Operand vonandalso, dietrue-Klausel voncase Test ofund was auf einen comprehension-Filter folgt, sehen den Test als wahr. Ein leerer Schnitt macht die Klausel unmöglich: sie trägt nichts zum Ergebnis bei. Eingeengte Fakten gelten nur innerhalb ihrer Klausel oder ihres Operanden;catch, der rechte Operand vonorelseund jede Klausel stellen die Fakten von davor wieder her. Nach einemcase,if,receive,tryodermaybeist der Fakt einer Variable die Vereinigung ihrer Fakten am Ende jeder Klausel, die abschließt (step 58H), daher hinterlässtcase X of forever -> ...; N when is_integer(N), N >= 0 -> ... endXalsnon_neg_integer() | forever. tryundmaybe(step 58J1): eintryist die Vereinigung der Werte seinerof-Klauseln (ohneofder seines Rumpfes) und der Werte seiner catch-Klauseln; derafter-Rumpf trägt nichts bei.of-Klauseln beginnen mit den Fakten am Ende des Rumpfes und matchen dessen Wert wiecase-Klauseln (eine unmögliche Klausel trägt nichts bei); catch-Klauseln und derafter-Rumpf beginnen mit den Fakten vor demtry, wobei ein Klassenmustererror | exit | throwmatcht. Einmaybeist die Vereinigung des Werts seines Rumpfes, der Werte seinerelse-Klauseln und, ohneelse, der Werte, an denen seine?=-Matches scheitern können: der Fakt des gematchten Werts ohne die Form des Musters, wenn das Muster seine gesamte Form matcht (neue, einmal verwendete Variablen, literale Atome und Ganzzahlen,[], Tupel daraus), sonst der ganze Fakt. Jedes?=-Muster bildet den Schnitt mit seinem Wert und veröffentlicht seine Variablen für den Rest des Rumpfes; ein?=, das nie matchen kann, beendet den Rumpf.else-Klauseln beginnen mit den Fakten vor demmaybeund matchen die vereinigten Fehlschlagswerte wiecase-Klauseln. Die Fakten nach einemtryvereinigen die am Ende seines Rumpfes (ohneof) und jeder abschließenden Klausel; nach einemmaybedie am Ende seines Rumpfes, jeder abschließendenelse-Klausel und, ohneelse, die davor.- Verwendungen (step 58H,
semantic/types/inference_uses): eine Operation, die eine Ausnahme auslöst, sofern ein Operand keinen bestimmten Typ hat, beweist diesen Typ für die gelesene Variable, nachdem die Operation zurückkehrt: Arithmetik und unäres-/+einnumber(),div,rem, Bitoperatoren undbnoteininteger(),and/or/xor,notund der linke Operand vonandalso/orelseeinboolean(),++und--einelist(), ein aufgerufener Wert ein fun dieser Stelligkeit, ein variabler Modul- oder Funktionsname einatom(), ein Map-Updatemap(), ein Tupel-record-Zugriff oder -Update das Tupel des records, ein Binary-Segment seinen Typ (<<X:8>>:integer()) und seine Größe einnon_neg_integer(), und eine Zeile pro Argumentprüfung eines Bridge-Builtins (hd/1,tl/1nonempty_maybe_improper_list(),length/1list(),element/2pos_integer()undtuple(),map_get/2undis_map_key/2map(),atom_to_list/1atom(), ...). Die Operation behält ihre Laufzeitprüfung. An denselben Wert gebundene Namen (Y = X, ein variablescase-Muster auf einem variablen Prüfausdruck) werden gemeinsam eingeengt. - Eintritts- und Erfolgsdomänen: die Eintrittsdomäne jedes Arguments ist die
Vereinigung über die möglichen Funktionsklauseln seines Fakts nach Kopf und
Guard (der eingeengte Fakt einer einfachen Variable, sonst der des Musters);
seine Erfolgsdomäne (step 58H) die Vereinigung über die abschließenden
Klauseln seines Fakts bei ihrer normalen Rückkehr.
--print-typeszeigt die Erfolgsdomäne als Eingaben (bounded(1..10) -> 1..10,inc(X) -> X + 1alsinc(number()) -> number()), bzw. die Eintrittsdomäne einer Funktion, die nie zurückkehrt. Ein zurückkehrender Aufruf engt seine variablen Argumente auf die Domäne des Aufgerufenen ein, außer innerhalb einer rekursiven Komponente, die noch gelöst wird. - Klauselergebnisse werden konservativ vereinigt: eine Projektion bleibt nur
erhalten, wenn jede Klausel dasselbe Argument liefert. Ein
caseoderifvereinigt seine Klauselergebnisse auf dieselbe Weise; eine Bindung, die von mehreren seiner Klauseln definiert wird, bleibtterm(). - Funktionstypen (step 58K,
semantic/types/function_types): neben der obigen Union-Zusammenfassung behält eine Funktion einen Funktionstyp pro möglicher Klausel, wie die Überladungen einer-spec: die Fakten der Argumente nach Kopf und Guard (der eingeengte Fakt einer einfachen Variable, sonst der des Musters) und das Ergebnis der Klausel (none()für eine Klausel, die immer eine Ausnahme auslöst; unmögliche Klauseln tragen keinen bei). Eine Klausel, deren Rumpf mit einemcaseoderifendet, teilt sich in einen Funktionstyp pro möglichem Zweig auf, mit den Fakten der Argumente nach Muster und Guard dieses Zweigs und dem Ergebnis des Zweigs (eine Ebene: ein verschachteltescaseteilt sich nicht weiter auf). Funktionstypen mit gleichen Eingaben werden zusammengeführt (ihre Ergebnisse vereinigt); über 8 hinaus werden die letzten zu einem zusammengeführt, Eingaben und Ergebnisse vereinigt. Rekursive Komponenten iterieren sie mit den Ergebnissen, wobei jede Runde das Ergebnis jedes Typs vereinigt (dann aufweitet); eine Komponente, die nicht konvergiert, behält nur ihre Union-Zusammenfassungen. Der Fakt eines anonymen fun behält einen Funktionstyp pro möglicher Klausel (seine Muster und sein Guard über ein beliebiges Argument),fun F/Adie Funktionstypen vonF/A; funs, die vereinigt werden, behalten ihre Funktionstypen nur, wenn sie gleich sind, sonst werden sie zu einem fun ihrer vereinigten Ergebnisse mit beliebigen Eingaben vereinigt (der Schnitt mit einem fun beliebiger Eingaben, wie ihn die VerwendungF(A)bildet, behält sie). Spezialisierung, Domänen und Spezifikationsprüfungen lesen die Union-Zusammenfassung. - Aufrufe wählen Funktionstypen aus (step 58L): ein Aufruf einer Funktion mit
Funktionstypen liest der Reihe nach jeden Typ, dessen Eingaben jeder
Argumentfakt schneidet, und hört nach einem exakten auf (Muster aus neuen,
einmal verwendeten Variablen, literalen Atomen und Ganzzahlen,
[]und Tupeln daraus; kein Guard oder nurtrueund Typtests einfacher Argumentvariablen; bei einem Zweig ein Argument alscase-Prüfausdruck), dessen Eingaben die Argumente enthalten: keine spätere Klausel kann betreten werden. Sein Ergebnis ist die Vereinigung der Ergebnisse der ausgewählten Typen, wobei ein Ergebnis, das einem Argument gleicht, der Fakt dieses Arguments innerhalb des Ergebnisses des Typs ist; wird keiner ausgewählt, ist der Aufrufnone()(er kann nurfunction_clauseauslösen). Unbekannte Argumente wählen jeden Typ aus, die Union; eine Funktion ohne Funktionstypen (Budget, nicht konvergierte Komponente) verwendet ihre Union-Zusammenfassung. Nachdem ein Aufruf zurückkehrt, werden seine variablen Argumente auf die Erfolgsdomäne und auf die vereinigten Eingaben der ausgewählten Typen eingeengt. Aufrufe von fun-Werten wählen die Funktionstypen des fun-Fakts auf dieselbe Weise aus, und ein anonymer fun, der für einen Aufruf erneut ausgewertet wird (siehe Funs), betritt, prüft und verlässt seine Klauseln wie eincase, sodass eine Klausel, die seine Argumente nicht matchen können, nichts beiträgt (F = fun(1) -> one; (_) -> other end, F(2)istother). Rekursive Komponenten konvergieren über Ergebnisse und Funktionstypen. - Aufrufe werten ihren Aufgerufenen erneut aus (step 58M): wenn die
Argumentfakten eines Aufrufs innerhalb der Eingaben des Aufgerufenen liegen
und in einer davon enger sind, wird der Rumpf des Aufgerufenen mit ihnen als
Eingaben erneut ausgewertet, wie ein gebundener anonymer fun, und das
Ergebnis des Aufrufs ist das Ergebnis der ausgewählten Funktionstypen,
geschnitten mit dem dieser Auswertung (
two_callers() -> {add_one(10), add_one(20)}ist{11, 21}); die Argumente des Aufrufs werden außerdem auf die Erfolgsdomäne dieser Auswertung eingeengt. Zusammenfassung, Funktionstypen und erfasste Ausdrucksfakten des Aufgerufenen ändern sich nie (die Spezialisierung liest nur diese). Budgets: 4 verschachtelte Auswertungen, Aufgerufene mit höchstens 256 Ausdrücken, 4.096 Arbeitseinheiten pro Aufruf aus einem Pool von 262.144 pro Durchlauf über den Batch (getrennt vom eigenen Budget des Batches); rekursive Komponenten werden nie erneut ausgewertet. Jenseits eines Budgets behält der Aufruf das Ergebnis der ausgewählten Funktionstypen. - Ein
receiveist die Vereinigung seiner Klauseln und seinesafter-Rumpfes, den ein Timeout voninfinitynie ausführt. - Eine rekursive Komponente beginnt das Ergebnis jedes Mitglieds bei
none()und inferiert alle Mitglieder erneut, bis sich kein Ergebnis mehr ändert; jede Runde vereinigt das neue Ergebnis mit dem vorherigen (weitet es nach den ersten 8 Runden auf, Inferenzdomäne), und ein ausstehender rekursiver Aufruf trägt nichts zu einer Vereinigung bei. Eine Funktion, die nie zurückkehren kann, bleibtnone(). Eine Komponente, die nach 8 Runden plus 4 pro Mitglied nicht konvergiert ist, weitet jedes Mitglied aufterm()auf (als aufgeweitet gemeldet, wie bei Budgeterschöpfung), und eine letzte Runde berechnet die Ausdrucksfakten neu; die Ausdrucksfakten früherer Runden werden verworfen, sodass nur Fakten aus den endgültigen Annahmen übrig bleiben. - Ein gemeinsames Arbeitsbudget begrenzt die Inferenz; Erschöpfung kostet Präzision und fällt auf generischen Code zurück, weist aber nie ein Programm zurück.
- Spezifikationen, die der Inferenz widersprechen, sind Fehler (step 58I,
semantic/types/contracts). Ein deklarierter Typ wird zu den Fakten, die er enthält (oder mehr): eingebaute Typen nach Namen (byte()ist0..255,timeout()istnon_neg_integer() | infinity,iodata()undiolist()Listen beliebiger Form), Aliase nach ihrer Definition, opake und nominale Typen nach ihrer Definition innerhalb ihres Moduls und als beliebiger Term außerhalb, Remote-Typen über den Batch, Typvariablen über ihrewhen-Schranken (eine unbeschränkte ist ein beliebiger Term), Maps und records nach ihrer Kategorie. Eine Spezifikation widerspricht dem Code, wenn ihre Fakten keinen Wert mit dem teilen, was die Inferenz beweist: dem inferierten Ergebnis (außer unbekannt, odernone(): eine Funktion, die nie zurückkehrt, passt zu jedem Ergebnis), der Eintrittsdomäne eines Arguments oder den Argumentfakten eines Aufrufs gegenüber jeder Überladung.none()/no_return()lässt nur eine Funktion zu, die nie zurückkehrt. Der Fehler nennt die Funktion, den deklarierten Typ wie geschrieben und den inferierten. Da inferierte Fakten mehr Werte enthalten können, als der Code erzeugt, ist nur ein disjunktes Paar ein Widerspruch: ein deklarierter Typ, der enger als der inferierte ist, wird akzeptiert.-callback-Spezifikationen werden nicht geprüft. OTPs Compiler prüft Spezifikationen nicht (Unterschiede).
Inferenzdomäne
Entscheidung von Plan 11 step 58A (semantic/types/lattice). Ein Fakt ist eine
Menge von Werten, die eine Variable oder ein Ergebnis haben kann. Fakten werden
dort vereinigt, wo der Kontrollfluss zusammenläuft (Klauseln, Zweige), und
zwischen den Runden einer rekursiven Komponente aufgeweitet; jedes der
folgenden Budgets weitet korrekt auf eine größere Menge auf und weist nie ein
Programm zurück. Fakten werden als Erlang-Typen ausgegeben, Kategorien unter
ihren eingebauten Namen.
| Fakt | Ausgabe | Vereinigung | Budget und Aufweitung |
|---|---|---|---|
| Nichts | none() | Neutrales Element | Eine Funktion, die nie zurückkehrt, bleibt none() |
| Alles | term() | Absorbiert jeden Fakt | dynamic() und any() sind term() |
| Ganzzahlen | 42, 1 | 3 | 7 | Union der Singletons | Mehr als 8 Singletons werden zu ihrem Bereich |
| Ganzzahlbereich | 1..10, 0..255 | Kleinster Bereich, der beide enthält | Eine Schranke, die sich zwischen Runden bewegt hat, springt zum nächsten Schwellenwert: eine untere auf 1, dann 0, dann unbeschränkt; eine obere auf -1, dann unbeschränkt |
| Unbeschränkte Ganzzahlen | pos_integer() (ab 1), non_neg_integer() (ab 0), neg_integer() (-1 und darunter), integer() | Kleinstes Intervall, das beide enthält, ausgegeben nach seiner Kategorie | — |
| Gleitkommazahlen | float() | — | — |
| Zahlen | number() | Ein Bereich oder eine Kategorie von Ganzzahlen, vereinigt mit float() | Singleton-Ganzzahlen mit float() bleiben 1 | float() |
| Atome | ok, error | ok, boolean() | Union der Singletons; genau false und true werden als boolean() ausgegeben | Mehr als 8 Singletons werden zu atom() |
| Bezeichner | pid(), port(), reference() | — | — |
| Tupel | {ok, 1}, tuple(), #point{x :: 0, y :: _} | Tupel gleicher Größe, deren erste Elemente nicht zwei verschiedene Atome sind (ihr Tag), werden elementweise vereinigt; andere bleiben getrennte Mitglieder | Mehr als 16 Elemente werden zu tuple(), sofern nicht jedes Element bekannt ist; mehr als 8 getrennte Formen werden zu tuple(). Ein Tupel mit Name und Größe eines sichtbaren Tupel-records wird als record ausgegeben (step 58J) |
| Listen | [], [T], [T, ...], nonempty_improper_list(H, T), [1, a], [a, b | T] | Elemente werden vereinigt; [] mit einer nichtleeren Liste ergibt eine möglicherweise leere; unechte Listen vereinigen Köpfe und Tails. Listen aus zwei oder mehr bekannten Elementen behalten ihre Positionen (step 58J; eine Clause-Notation, die Typsprache hat keine, Elemente mit | in Klammern ausgegeben): positionelle Listen einer Länge werden Position für Position vereinigt, sonst als einfache Listen | Eine Liste von 0..1114111 (char()) wird als string() oder nonempty_string() ausgegeben; eine möglicherweise leere Liste von _ als list() |
| Maps | #{}, #{a := 1}, #{1..17 => a}, map() | Maps mit denselben Schlüsseln werden Wert für Wert vereinigt; Maps anderer Schlüssel werden zu einer Assoziation ihrer vereinigten Schlüssel und Werte vereinigt (=>: jeder Schlüssel darf fehlen, step 58J) | Mehr als 16 Schlüssel werden zu einer Assoziation vereinigt |
| Funs | fun((term()) -> 1), fun() | Funs einer Stelligkeit vereinigen ihre Ergebnisse; andere Stelligkeiten ergeben fun() | — |
| Bitstrings | <<_:16>>, <<_:3, _:_*2>>, binary() | Die kürzere Größe plus jede Größendifferenz als Einheit | Basis und Einheit 0/8, 8/8, 0/1, 1/1 werden als binary(), nonempty_binary(), bitstring(), nonempty_bitstring() ausgegeben |
- Eine Union behält ein Mitglied pro vereinigter Form, in der
Erlang-Termordnung ihrer Werte: Zahlen, Atome,
reference(), funs,port(),pid(), Tupel, Maps,[], Listen, bitstrings, dann deklarierte benannte Typen; mehr als 8 Mitglieder werden zuterm(). - Container sind höchstens 4 Ebenen tief verschachtelt; ein tiefer liegender
Fakt wird zu
term(). - Eine rekursive Komponente vereinigt Ergebnisse 8 Runden lang (Zyklen von bis
zu 8 Funktionen konvergieren exakt) und weitet sie dann auf. Eine Komponente,
die nach 4 weiteren Runden pro Mitglied nicht konvergiert ist, weitet jedes
Mitglied auf
term()auf und wird als aufgeweitet gemeldet, wie ein erschöpftes Budget. - Einengung (plan steps 58G, 58H,
Lattice::meet) ist der Schnitt von Fakten, die Werte, die beide enthalten (oder mehr, abernone()nur, wenn sie keine teilen): Muster und Guards (Typtests wieis_integer/1engen ihr Argument auf die Kategorie ein) engen innerhalb ihrer Klausel ein und legen die Eintrittsdomäne einer Funktion fest; eine Verwendung, die eine Ausnahme auslöst, sofern ihr Operand keinen bestimmten Typ hat, engt den Operanden danach auf dem normalen Pfad ein. Ein leerer Schnitt bedeutet, dass der Pfad nicht ausgeführt werden kann. - Spezifikationen fügen Fakten nie etwas hinzu: inferierte Fakten stammen nur aus Code und entscheiden nie auf das Wort einer Spec hin über eine Repräsentation.
Das Lowering verarbeitet diese Fakten. Erzeugte IR wandelt nie eine Ganzzahl in einen Heap-Zeiger um; jedes fehlbare Serviceergebnis wird nur auf seinem Erfolgspfad geladen, und Formprüfungen dominieren die Extraktion. Spezialisierungsstrategie: specialization.md.
--print-types
Gibt jedes Modul des Batches (in Eingabe-/Zielreihenfolge, Bibliotheksmodule danach) als Erlang-Quelltext aus (Quelltextausgabe) zusammen mit dem, was die Typinferenz gefunden hat. Die Ausgabe erfolgt auf stdout und ist menschenlesbar, weder Erlang noch ein Austauschformat. Warnungen bleiben auf stderr.
%% module "branches" source="branches.erl" target="" declared=complete inferred=complete
-module(branches).
-export([mixed/1]).
%% inferred: mixed(_) -> 1..2
mixed(X) ->
case X of
1 ->
1;
_ ->
2
end :: 1..2.
- Eine
%% module-Zeile nennt das Modul, seinen Quelltext, das Projektziel und ob deklarierte und inferierte Typen vollständig waren oder durch ein Limit aufgeweitet wurden. - Deklarationen (
-type,-spec,-callback, records) erscheinen wie geschrieben. - Über jeder Funktion gibt
%% declared: f(Inputs) -> Resultjede Überladung ihrer-specaufgelöst an (mit ihrenwhen-Bedingungen), und%% inferred: f(Inputs) -> Result, was die Inferenz gefunden hat, sodass beide verglichen werden können. Die Eingaben sind die Erfolgsdomäne jedes Arguments (ausgehend vonterm()bei exportierten Funktionen und solchen, diefun F/Abenennt, ausgehend von den vereinigten Argumenten der Aufrufer bei anderen Funktionen). Eine Funktion mit mehreren Funktionstypen gibt eine Signatur pro Typ aus, jede mit eigenen Eingaben:f(integer()) -> integer(); (atom()) -> string()(semantic::types::function_source). Eine-specbleibt direkt vor der Funktion, durch eine Leerzeile von anderen Formen abgesetzt. - Ausdrücke, deren Fakt mehr als
term()aussagt, werden alsExpression :: Typeannotiert (ein bekannter Typ verdeckt eine Argumentrelation, die nur bei einem Wert erscheint, über den sonst nichts bekannt ist): in Klammern innerhalb anderer Ausdrücke, ohne sie bei einem ganzen Rumpfausdruck. Literale Terme (Literale sowie Tupel, Listen, konstruierte Maps und bitstrings aus Literalen) und Matches werden nicht annotiert (die rechte Seite eines Matches schon). - Ein Wert, den die Inferenz als gleich einem der Argumente der Funktion
bewiesen hat und über den nichts weiter bekannt ist, wird als Name dieses
Arguments ausgegeben, eine Typvariable: die Variable, die ihm die erste
Klausel gibt, die das ganze Argument bindet, sonst
_argumentN(1-basiert, auch wenn ein früheres Argument den Namen genommen hat). Die Eingabe des Arguments zeigt denselben Namen, wenn sie ein beliebiger Term ist:second(_, Y) -> Y,keep(Acc, number()) -> Acc. Eine Variable zeigt nur ihren Typ, ihr Name sagt bereits, welches Argument sie ist.
Inferenzerwartungen
tests/fixtures/inference/*.erl halten fest, was die Inferenz für jede
Funktion finden sollte und was sie heute findet. Jedes Modul wird zum CTest
inference_<module> (tests/compiler/inference/expectations.py):
%% expect: sum() -> 3
%% today: sum() -> _
sum() -> 1 + 2.
expect:ist die Signatur, die--print-typesin seiner%% inferred:-Zeile ausgeben sollte;today:, vorhanden, solange die Inferenz dahinter zurückbleibt, ist die, die es jetzt ausgibt.- Die Prüfung vergleicht die Ausgabe mit
today, falls vorhanden, sonst mitexpect; jede Funktion des Moduls braucht eineexpect-Zeile. Einetoday-Zeile, die die Inferenz eingeholt hat, lässt die Prüfung fehlschlagen, bis sie entfernt wird. expectations.py <clau> <fixture> --recordschreibt dietoday-Zeilen aus der aktuellen Ausgabe neu (eine Aktion für Maintainer: den Diff prüfen).values.erldeckt Literale, Arithmetik und Vergleiche ab, Aufrufe lokaler und anderer Funktionen, Ganzzahlvereinigungen und -bereiche, Ganzzahlen oder Gleitkommazahlen, Listen, Strings, Tupel, Maps mit Atom- und anderen Schlüsseln, zurückgegebene und angewendete funs, binaries, Argumentrelationen undtry/maybe-Werte. Heute findet die Inferenz literale und konstruierte Werte, Ergebnisse von Operatoren und Builtins, Container und ihre Teile, funs und ihre Aufrufe, lokale Eingaben von Aufrufern, Ganzzahlvereinigungen und Argumentrelationen, Einengung durch Verwendungen und Erfolgsdomänen (141 von 141 Funktionen).narrowing.erldeckt jeden Typtest ab, case-Guards, Prüfausdrücke mit wahrem Test,andalso, comprehension-Filter, Tupel-/Listen-/Map-Muster, Auffangklauseln, Bereichs-Guards, Widersprüche, Disjunktionen, die Klausel nach einem einzelnen Typtest, Einengung nach einem Aufruf, Einengungen, die nicht durchsickern dürfen, und Vergleichsbereiche: jeden Operator auf beiden Seiten, zwei Variablen,=/=an und innerhalb einer Schranke, Komplemente, case- und if-Guards, einorelse, einen Countdown mit Guard und Operanden, die nicht eingeengt werden dürfen (51 von 51).base_types.erlhat eine Funktion pro Basistyp und eingebautem Typ der Typsprache (pid(),reference(), bitstrings und binaries, Bereiche,byte(),char(),non_neg_integer(),boolean(),string(),iolist(),mfa(),timeout(),no_return(), ...): ihre-specnennt den Typ, sodass für jeden eingebauten Typ geprüft wird, dass er sich auflöst, und ihr Rumpf erzeugt einen solchen Wert. Kategorien werden unter ihren eingebauten Namen erwartet, beschränkte Ganzzahlmengen als Bereiche. Jede Funktion erreicht ihren erwarteten Typ.clauses.erldeckt Funktionstypen ab (step 58K): Typtests und literale Muster pro Klausel, zusammengeführte gleiche Eingaben, eine Klausel, die Aufrufer nie betreten, mehr Klauseln als das Budget, eine rekursive Funktion, eine einzelne Klausel, eincaseund einifam Ende des Rumpfes, ein verschachteltescaseund eincase, das nicht am Ende steht, anonyme funs mit mehreren Klauseln,fun F/A, Vereinigungen gleicher und verschiedener funs und eine Klausel, die immer eine Ausnahme auslöst; ferner die Aufrufauswahl (step 58L): einen Typtest, einen ersten exakten Zweig, unbekannte Argumente, keinen zugelassenen Typ, Einengung nach dem Aufruf, ein literales Argument, einen Bereich über zwei Klauseln, verschachtelte Aufrufe, eine lokale Funktion mit mehreren Aufrufern, einen rekursiven Aufgerufenen und funs mit mehreren Klauseln, gebunden, in einem Tupel, an eine lokale Funktion übergeben und mit einem unbekannten Argument aufgerufen; und die Auswertung pro Aufruf (step 58M): Ergebnisse pro Aufruf, Verschachtelung bis zum und über das Tiefenbudget hinaus, einen Aufgerufenen über dem Größenbudget, einen rekursiven Aufgerufenen und unbekannte Argumente.
Ausgabe von Typen
semantic::types::type_source(graph, type) (semantic/types/printing) stellt
einen Typ des Typgraphen in Erlang-Typsyntax dar: _ für einen beliebigen Term
(term(), der Kürze halber von TERM_SOURCE geschrieben; die Typsyntax liest
_ als any()), none(), Atome und Ganzzahlen, 1..5, {ok, T}, tuple(),
[T], [T, ...], #{K => V, K := V}, #r{f :: T}, <<_:B, _:_*U>>,
fun((A) -> R), A | B. Ein fun mit mehreren Funktionstypen gibt sie in einer
Clause-Notation aus, fun((1) -> one; (_) -> other): die Erlang-Typsyntax
kennt keinen überladenen fun-Typ, und eine Union von fun-Typen bedeutet etwas
anderes. Die Ganzzahlen einer Union werden in Wertreihenfolge dort ausgegeben,
wo ihre erste Ganzzahl steht, aufeinanderfolgende als Bereich (1 | 2 | 3 | 5
wird als 1..3 | 5 ausgegeben; der Fakt behält die Singletons).
Vordefinierte erlang-Typen verlieren ihr Modul; Referenzen auf deklarierte
Typen bleiben benannt. Ein Knotenbudget begrenzt den Text; jenseits davon, und
unterhalb von 32 Verschachtelungsebenen, steht ... an seiner Stelle.
clau --print-types answer.erl client.erl
clau --print-types --project project.toml --target demo --verbose
Clause