Semantische Analyse
Läuft nach dem Parsen bei der Standardkompilierung und bei --print-types;
rein syntaktische Aktionen überspringen sie. Fehler stoppen den betroffenen
Batch vor LLVM; Diagnosen behalten ihre Herkunft aus Makros und Includes, und
spätere Eingaben werden trotzdem diagnostiziert.
Modul- und Aufrufprüfungen
- Moduldeklaration erforderlich und eindeutig; Funktionsstelligkeiten 0..255; keine doppelten Definitionen; jeder Export existiert (ein wiederholter Export warnt, wie in OTP). Gequotete/Unicode-Namen behalten ihre exakte Identität.
- Jede Funktion wird geprüft, auch ungenutzter und unerreichbarer Code.
- Aufrufe werden innerhalb des Batches nach Modul/Name/Stelligkeit aufgelöst. Entfernte Aufrufe (auch selbstqualifizierte) erfordern Exporte. Fehlende oder 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 stark zusammenhängende Komponenten zerlegt (iterativer Tarjan), in der Reihenfolge Aufgerufener vor Aufrufer; eine Komponente ist rekursiv, wenn sie mehrere Mitglieder hat oder ein Mitglied sich selbst aufruft.
Behaviours
Folgt den
Behaviours
von OTP, erl_internal:add_predefined_functions und check_behaviour von
erl_lint.
- Ein Modul, das
-callbackdeklariert, erhält das vordefinierte, exportiertebehaviour_info/1von OTP:callbacksliefert die deklarierten Callbacks in Quelltextreihenfolge,optional_callbacksdie der wohlgeformten-optional_callbacks-Listen (mit einem Modul qualifizierte Callbacks werden weggelassen). Das Frontend hängt es als Funktion an der-module-Zeile an (driver/predefined). Wie in OTP kann der eigene Quelltext des Moduls es nicht benennen: ein-export, ein lokaler Aufruf,fun behaviour_info/1oder eine-specdavon ist eine undefinierte Funktion; entfernte Aufrufe (auch selbstqualifizierte) erreichen es.-callbackneben einem handgeschriebenenbehaviour_info/1ist ein Fehler (cannot define callback attibute for F/A when behaviour_info is defined, die Schreibweise von OTP). -behaviour(M)und-behavior(M)dürfen sich wiederholen.Mwird im Batch aufgelöst; ein Name mit einem Bibliotheksmodul (library.md) fügt dieses Modul hinzu, wie es ein literaler Aufruf tut. Diagnosen verwenden den Text von OTP am Attributnamen:undefined callback function F/A (behaviour 'M')für jeden erforderlichen, nicht optionalen Callback, den das Modul nicht exportiert (sortiert);conflicting behaviours -- callback F/A required by both 'M2' and 'M1' (line L, column C), wenn ein früheres Behaviour ihn ebenfalls verlangt (ein optionaler Callback zählt, wenn er exportiert ist);behaviour M undefined, wennMnicht im Batch ist oder keinbehaviour_info/1hat (ein OTP-Behaviour, das die Bibliothek von Clause noch nicht bereitstellt, etwaapplication), dazu die Modulnamensfehler von OTP für Namen, die keine Module sein können (the module name must not be empty,... must be an atom, ...).
- Warnungen lassen die Kompilierung nicht fehlschlagen. Die
-compile-Optionennowarn_behaviours(die ganze Prüfung, Modulnamensfehler eingeschlossen),nowarn_undefined_behaviour_func,nowarn_undefined_behaviourundnowarn_conflicting_behavioursschalten sie ab. - Nicht ausgewertet: ein handgeschriebenes exportiertes
behaviour_info/1eines Behaviour-Moduls, daher werden die Callbacks seiner Nutzer nicht geprüft, und die Warnungen von OTP zu fehlerhaft definierten und veralteten Callbacks treten nie auf (Unterschiede). - Belege:
tests/fixtures/lint(von OTP erzeugte Diagnosen, CTestlint_diagnostics),executables_behaviours, CLI-Fälle vonsemantic.
Importe
Folgt den Importregeln von erl_lint.
-import(Module, [F/A, ...])macht einen lokalen AufrufF(...)zum entfernten AufrufModule:F(...): Das Modul muss im Batch sein (ein Bibliotheksmodul kommt hinzu wie bei einem literalen Aufruf) und die Funktion exportieren, oder ein Builtin-Modul sein (-import(io, [format/2]),-import(erlang, [display/1])). Ein Import hat Vorrang vor einem automatisch importierten BIF gleichen Namens.- Fehler mit dem Text von OTP:
function F/A already imported from M(das ganze Attribut wird dann ignoriert),defining imported function F/A,creating a fun from imported name F/A is not allowed. Eine importierte Funktion in einem Guard ist ein unzulässiger Guard-Aufruf (außer Guard-BIFs von erlang). - Warnung
import directive overrides auto-imported BIF F/A -- ..., es sei denn,no_auto_importnennt ihn odernowarn_bif_clashist angegeben; Clause kennt die automatisch importierten BIFs, die es implementiert. - Belege:
executables_imports, Lint-Fälleimports_conflicts,imports_fun,imports_bif, semantische CLI-Fälle.
Vordefinierte Funktionen
Folgt den Modulen von OTP und
erl_internal:add_predefined_functions. Das Frontend hängt an jedes
kompilierte Modul (nach einem erzeugten behaviour_info/1) module_info/0,1
als Erlang-Funktionen an, die literale Daten zurückgeben (driver/predefined,
semantic/module_info); Module::predefined_ zählt diese nachgestellten
Formen, die immer exportiert und aus --print-types weggelassen werden.
module_info()liefert[{module,_},{exports,_},{attributes,_},{compile,_},{md5,_}];module_info(Key)beantwortet außerdemfunctions,nifs([]) undnative(false) und löst für jeden anderen Schlüsselbadargaus.exports: die exportierten Funktionen in Definitionsreihenfolge (Escripts fügenmain/1hinzu), dannbehaviour_info/1, wenn erzeugt,module_info/0,module_info/1.functions: jede Funktion in Definitionsreihenfolge, dann dieselben vordefinierten.attributes: jedes Attribut außermodule, Exporten und Importen, Type-/Spec-/Callback-Formen, Records,export_type,optional_callbacks,export_record,compile,file, Dokumentation undfeature, in Quelltextreihenfolge; ein Wert, der keine Liste ist, wird in eine gehüllt (-tags(a)ist{tags,[a]}). Ohne-vsnkommt{vsn,[N]}zuerst, wobeiNdas als vorzeichenlose Big-Endian-Ganzzahl gelesenemd5ist, wie OTP es ableitet.md5: MD5 des ausgegebenen Quelltexts des Moduls (print_source: Makros expandiert, Includes eingefügt, Kommentare und Layout entfernt).compile:[{version,ClauseVersion},{options,[]},{source,AbsolutePath}].- Anders als
behaviour_info/1sieht der eigene Quelltext des Moduls sie: lokale Aufrufe undfun module_info/1funktionieren. Eine der beiden zu definieren ist ein Fehler (function module_info/0 already defined); eine zu exportieren warnt, wie jeder wiederholte Export, nur (function module_info/0 already exported). - Attribute sind Daten; nur
on_load,nifsund nicht wirkungslose-compile-Optionen werden als verhaltensändernd abgewiesen. - Die erzeugten Funktionen haben keine Quelltextzeile (OTP gibt ihnen keine):
keine Debug-Orte, IR-Quelltextkommentare oder Einträge in
--print-types.module_info/0enthält dieselben Literale wiemodule_info/1und macht daher keine Aufrufe. - Belege:
executables_module_info(stdout von OTP; abgeleitete Werte nach Form), Fällemodule_info_defined,exports_repeatedintests/fixtures/lint.
Bindungen
Jede Bindung hat eine funktionsrelative Identität clause[N].local[M].
Vorkommen sind Definitionen, Lesezugriffe oder Prüfungen auf exakte Gleichheit,
markiert mit dem Kontext Kopf/Guard/Rumpf. Die Analyse ist deterministisch.
- Jede Klausel beginnt mit einer leeren Umgebung. Kandidaten im Kopf 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 laufen von
rechts nach links. Ein geklammertes Muster
P1 = P2ist ein Alias, keine Sequenz. - Geschwisterausdrücke lesen dieselben eingehenden Namen; ihre Definitionen
werden nach dem ganzen Ausdruck exportiert.
{X = 1, X}ist ein ungebundener Lesezugriff;{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 Rumpfs, catch-Klauseln sehen sie als unsicher, und der after-Rumpf sieht jeden früher 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 Rumpfs als unsicher, und jeder darin gebundene Name ist danach unsicher. Guards veröffentlichen nie Bindungen; Matches in Guards sind Fehler, auch wenn sie unerreichbar sind. - Map-Schlüssel lesen nur eingehende Bindungen; Binary-Größen lesen außerdem frühere Segmente desselben Binary (siehe Muster).
- Ein
case-Prüfausdruck bindet im umschließenden Gültigkeitsbereich. Jede Klausel beginnt mit diesem Bereich; ihre Musterdefinitionen sind vorläufig bis zum Guard, 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 beierl_lintvon OTP (icrt_export); die Warnung von OTP, 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 verortete Fehler. Die
Meldungen behalten den Wortlaut des Compilers (
unbound variable X,unsafe variable X); das Bindungskorpus prüft jede gegen die Klasseunbound_var/unsafe_varvon OTP. - Eine Comprehension wertet jeden Qualifier der Reihe nach in dem 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 der davor: Ihre Namen sind dort ungebunden.
- Die Klauseln eines anonymen Funs beginnen jeweils mit dem Gültigkeitsbereich
am Fun: Kopfnamen sind neu und verdecken äußere, Guards lesen sie, und nichts
darin Gebundenes ist nach dem Fun sichtbar. Jede darin gelesene äußere
Definition wird als Erfassung festgehalten (
Function::captures, Definitionsreihenfolge). Der Name eines benannten Funs 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
oder 32 pro Ausdruck, wenn das mehr ist (erzeugte Parser wie erl_parse von
OTP brauchen etwa 10 pro Ausdruck). Die Dienstauflösung hat ein Budget gleicher
Größe; sie durchsucht die -compile- und -import-Formen des Moduls nach dem
Auto-Import-Zustand eines Namens einmal pro Name, nicht einmal pro Aufruf (jede
der Tausenden Klauseln von erl_parse ruft hd/1 auf). 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 oben und none() unten. Standards: 1.048.576
Knoten, 256 Union-Mitglieder, 200.000 Arbeitseinheiten pro Durchlauf über einen
deklarierten Typ (Auflösung, Übersetzung, Expansion) und 8.000.000 pro
Inferenzdurchlauf über das ganze Programm; ein Debug-Build von clau (ohne
NDEBUG) erlaubt 16.000 und 1.000.000, damit große Programme trotzdem schnell
kompilieren, und kann stärker aufweiten. Erschöpfung weitet mit sichtbarem Flag
auf term() auf und engt nie eine Repräsentation ein.
Deklarierte Metadaten (-type, -opaque, -nominal, -export_type,
-spec, -callback, -optional_callbacks) folgen der Typespec-Referenz von
OTP und dem festgelegten Verhalten von erl_lint/erl_internal/erl_types:
- Lokale Aliase dürfen Namen eingebauter Typen verdecken. Entfernte Typen des
Batches müssen exportiert sein. Fehlende Typen, Duplikate, fehlerhafte
Metadaten, Singleton-Typvariablen, ungültige Schranken und Specs für fehlende
Funktionen sind Fehler. Nicht verfügbare externe Typen warnen und werden zu
term(). - Jeder Alias und jede Überladung hat einen eigenen Variablenbereich; wiederholte formale Parameter folgen der Ersetzung durch das letzte Argument wie in OTP.
- 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 Auswertung von Konstanten ist exakt, begrenzt auf 10.000 Dezimalziffern 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 (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. Jede Eingabe steigt je nach Anzahl ihrer Änderungen eine Stufenleiter hinauf: Ihre ersten 4 Änderungen vereinigen, die nächsten 2 weiten wie rekursive Ergebnisse auf, die nächsten 2 ersetzen zusätzlich Container innerhalb eines Containers durchterm(), und eine weitere Änderung ergibtterm()(als aufgeweitet gemeldet). Eingaben, die sich nach 63 Durchläufen nicht stabilisiert haben, werden zuterm()(als aufgeweitet gemeldet). Eine rekursive Komponente gilt als kostspielig, sobald eine Lösung mehr als 2^17 Gittervereinigungen (verschachtelte mitgezählt) und mehr als 1/16 der bisher höchsten Anzahl von Vereinigungen eines Durchlaufs verbraucht; ändern sich die Eingaben ihrer Mitglieder erneut, werden sie sofort zuterm()(als aufgeweitet gemeldet), sodass spätere Lösungen den wechselnden Eingaben ihrer Aufrufer nicht mehr folgen. -
Durchläufe nach dem ersten durchlaufen nur Komponenten, für die sich etwas Gelesenes geändert hat (
semantic/types/inference_incremental): Eine Komponente behält ihre Zusammenfassungen und Ausdrucksfakten, solange die Eingaben ihrer Mitglieder, jede gelesene Zusammenfassung (Aufrufe,fun F/A,apply) und die Eingaben erneut ausgewerteter Aufgerufener unverändert sind und die Budgets für den Durchlauf und die erneute Analyse dieselben Antworten liefern würden (das Budget für die erneute Analyse ist genauso weit verbraucht wie zu Beginn ihres letzten Durchlaufs oder würde erneut alles gewähren oder verweigern); ihre Arbeit zählt weiterhin zum Durchlaufbudget. Die Ergebnisse entsprechen einem Durchlauf über jede Komponente. Der letzte Durchlauf mitterm()-Eingaben durchläuft alle. Nach einem Durchlauf werden nur die Eingaben der darin durchlaufenen oder von einer darin durchlaufenen Funktion aufgerufenen Funktionen erneut aus ihren Aufrufen vereinigt; die Argumentfakten und Eingaben der anderen bleiben wie bei der letzten Vereinigung. -
Literale haben exakte Fakten: Ganzzahlen jeder Größe und Zeichen sind Singletons, Atome Singleton-Atome, Gleitkommazahlen
float(), Strings nicht leere 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 (
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+,-,*nicht negativer Bereiche,bandmit einem nicht negativen 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 Brücken-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 entfernte Aufrufe mit frischen Variablen pro Aufruf.
-
Container (
semantic/types/inference_containers): Eine Liste[E1, ..., En | T]vereinigt ihre Elemente vor dem Fakt des Endes (eine echte Liste, wenn das Ende eine ist,nonempty_improper_list(H, T), wenn es keine Liste ist,nonempty_maybe_improper_list(), wenn es unbekannt ist oder unecht sein kann: Eine Zelle ist nie leer, auch in einem Muster wie[H | _]);++,--,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. Eine Map-Aktualisierung 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 ein (undefined, wenn keiner angegeben ist), Zugriff und Aktualisierung lesen und setzen das Feld der passenden Tupel, und#r.fist der Index des Felds. 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 (
semantic/types/inference_funs):fun F/Aist ein Fun seiner Stelligkeit, der das inferierte Ergebnis der Funktion zurückgibt (das eines Builtins oder, bei variabler Stelligkeit,fun()),fun M:F/Aeiner, derterm()zurückgibt, und ein anonymer Fun einer, der die vereinigten Ergebnisse seiner Klauseln zurückgibt, erfasste Werte eingeschlossen. Ein Aufruf eines Werts vereinigt die Ergebnisse seiner Funs dieser Stelligkeit, die seine Argumente auswählen (term()bei einem 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(F, Args)(aucherlang:apply/2) ist ein Aufruf vonFmit den Argumenten, dieArgsenthält: den Elementen einer literalen echten Liste, sonst denen ihres Fakts ([], eine Liste bekannter Positionen oder so viele Kopien des Elements einer echten Liste, wie die einzige Stelligkeit vonFnimmt); ein gebundener anonymer Fun wird wie oben erneut ausgewertet (apply(fun kind/1, [1])wähltkind(1)).apply(M, F, Args), dessenMundFbekannte Atome sind, die eine exportierte Batch-Funktion benennen, liest die Funktionstypen dieser Funktion wiefun F/A. Argumente unbekannter Länge, andere Ziele undM:F(Args)mit variablen Namen bleibenterm(). Dafun F/Aundapply/3eine später inferierte Funktion benennen können, wiederholen sich die Durchläufe über den Batch außerdem, bis jeder solche Lesezugriff das endgültige Ergebnis seiner Funktion gesehen hat; andernfalls gibt ein letzter Durchlauf ihnenterm()als Ergebnis. -
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 Rumpfs) und Generatoren (von den Elementen ihrer Eingabe oder Map-Schlüsseln und -Werten). Nicht bewiesene Werte bleibenterm()ohne Relationen. -
Einengung (
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 (eine Literal-, 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 prüft. 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) schneiden den 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 bewiesene Ganzzahlen, die verglichen werden, engen einander durch ihre Schranken ein (X > YmitYin0..5machtXmindestens 1), und/=,=/=mit einer Konstante rücken eine ihr gleiche Bereichsschranke nach innen. 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 ganzer Guard ein einzelner Typtest war, sieht diesen Wert ohne die geprüfte Kategorie; nach einem einzelnen Vergleich mit einer Ganzzahlkonstante sieht derselbe als Ganzzahl bewiesene Wert (eine einfache Variable dieser Klausel) 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 das, was auf einen Filter einer Comprehension 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, sodasscase X of forever -> ...; N when is_integer(N), N >= 0 -> ... endXalsnon_neg_integer() | foreverhinterlässt. -
tryundmaybe: Eintryist die Vereinigung der Werte seinerof-Klauseln (ohneofder seines Rumpfs) und der Werte seiner catch-Klauseln; derafter-Rumpf trägt nichts bei.of-Klauseln beginnen mit den Fakten am Ende des Rumpfs 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 Rumpfs, 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 ganze Form matcht (neue, einmal verwendete Variablen, literale Atome und Ganzzahlen,[], Tupel daraus), sonst der ganze Fakt. Jedes?=-Muster schneidet seinen Wert und veröffentlicht seine Variablen für den Rest des Rumpfs; ein?=, das nie matchen kann, beendet den Rumpf.else-Klauseln beginnen mit den Fakten vor demmaybeund matchen die vereinigten fehlschlagenden Werte wiecase-Klauseln. Die Fakten nach einemtryvereinigen die am Ende seines Rumpfs (ohneof) und jeder abschließenden Klausel; nach einemmaybedie am Ende seines Rumpfs, jeder abschließendenelse-Klausel und, ohneelse, die davor. -
Verwendungen (
semantic/types/inference_uses): Eine Operation, die eine Ausnahme auslöst, es sei denn, ein Operand hat einen bestimmten Typ, 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--einlist(), ein aufgerufener Wert einen Fun dieser Stelligkeit, ein variabler Modul- oder Funktionsname einatom(), eine Map-Aktualisierungmap(), ein Zugriff auf oder eine Aktualisierung eines Tupel-Records 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 Brücken-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 Prüfung zur Laufzeit. Namen, die an denselben Wert gebunden sind (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 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 Aufruf, der zurückkehrt, 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. -
Funktionstypen (
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 Ergebnis ein abhängiger Fakt über ihre Argumente ist (eincaseoderifan ihrem Ende oder eine daran gebundene Variable, in beliebiger Tiefe verschachtelt), teilt sich in einen Funktionstyp pro Klausel dieses Fakts auf, wobei die Fakten seiner Parameter mit den Argumenten, die sie benennen, geschnitten werden. Funktionstypen mit gleichen Eingaben werden zusammengeführt (ihre Ergebnisse vereinigt); über 32 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 Funs 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: 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 auf Ergebnisse und Funktionstypen. -
Aufrufe werten ihren Aufgerufenen erneut aus: Liegen die Argumentfakten eines Aufrufs innerhalb der Eingaben des Aufgerufenen und sind in einer enger, 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 Vorrat 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-Rumpfs, den ein Timeout voninfinitynie ausführt. -
Eine rekursive Komponente beginnt das Ergebnis jedes Mitglieds bei ihrer ersten Lösung bei
none()und in späteren Durchläufen bei den Zusammenfassungen, mit denen ihre letzte Lösung endete: Dabei ersetzt der erste Durchlauf jedes Mitglieds seine Zusammenfassung durch das gefundene Ergebnis (die frühere Zusammenfassung dient davor nur als Annahme für Aufrufe), sodass veraltete Funktionstypen nie erhalten bleiben. Sie inferiert Mitglieder neu, 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 oder deren Runden mehr als ein Achtel des Durchlaufbudgets verbraucht haben, 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 bleiben. Eine solche Komponente wird gemerkt: Spätere Durchläufe geben ihren Mitgliedern sofortterm()und führen nur diese letzte Runde aus, statt erneut bis zur Obergrenze zu iterieren. Der Durchlauf, der jede Eingabe aufterm()aufweitet, vergisst die gemerkten Komponenten, sodass jede mit diesen Eingaben noch einmal gelöst wird. -
Runden verkleinern nie die Zusammenfassung eines Mitglieds: Ihre Funktionstypen werden Typ für Typ mit denen der vorherigen Runde vereinigt (Ergebnisse vereinigt, geänderte Eingaben vereinigt, und der Typ ist nicht mehr exakt) oder, wenn sich die Anzahl der Typen geändert hat, werden die Typen beider Runden zusammengeführt. Als Sicherheitsnetz behält jedes Mitglied seine letzten 4 Zusammenfassungen, und eines, das zu einer davon zurückkehrt, ist für den Rest der Lösung
term()ohne Funktionstypen, was die anderen Mitglieder konvergieren lässt. -
Eine Runde durchläuft nur Mitglieder, die eine seit ihrem letzten Durchlauf geänderte Zusammenfassung lesen: Jeder Durchlauf merkt sich die Zusammenfassungen, die er liest (Aufrufe, Aufrufergebnisse,
fun F/A), eine geänderte Zusammenfassung markiert ihre Leser, und ein nicht markiertes Mitglied behält seine Ausdrucksfakten. Die erste Runde durchläuft jedes Mitglied. Ein Mitglied behält seinen letzten Durchlauf über Lösungen hinweg: Eine Lösung übernimmt dessen Ergebnis samt Ausdrucksfakten und Arbeit, solange die gelesenen Mitglieder dieselben Ergebnisse und Funktionstypen haben und alles andere Gelesene unverändert ist und dieselben Antworten liefern würde, wie bei einer beibehaltenen Komponente in inkrementellen Durchläufen. Die Ergebnisse entsprechen einem erneuten Durchlauf. -
Ein gemeinsames Arbeitsbudget begrenzt die Inferenz; Erschöpfung kostet Präzision und fällt auf generischen Code zurück, weist aber nie ein Programm ab.
-
Spezifikationen, die der Inferenz widersprechen, werden diagnostiziert (
semantic/types/contracts), werden aber nie zu Fakten, sodass sich das Programm in jedem Fall so verhält, wie sein Code sagt. 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 davon, entfernte Typen über den Batch, Typvariablen über ihrewhen-Schranken (ohne Einschränkung: ein beliebiger Term), Maps und Records nach ihrer Kategorie. Nur ein disjunktes Paar widerspricht, da inferierte Fakten mehr Werte enthalten können, als der Code erzeugt:- eine Eintrittsdomäne eines Arguments, die keinen Wert mit der Spec teilt, ist ein Fehler;
- ein inferiertes Ergebnis, das keinen teilt (außer wenn unbekannt oder
none(), was zu jedem Ergebnis passt), oder ein Aufruf, dessen Argumentfakten jede Überladung abweist, ist eine Warnung (der Aufruf kann auf einem Pfad liegen, der nie läuft); none()/no_return()lässt nur eine Funktion zu, die nie zurückkehrt.
Die Diagnose nennt die Funktion, den deklarierten Typ wie geschrieben und den inferierten.
-callback-Spezifikationen werden nicht geprüft. Der Compiler von OTP prüft keine Spezifikationen (Unterschiede).
Abhängige Fakten
semantic/types/dependent. Ein case, if oder try ... of verhält sich wie
ein Fun der Variablen, die seine Klauseln einengen, angewendet auf diese: Sein
Wert ist ein abhängiger Fakt, ein Funktionstyp pro möglicher Klausel.
- Parameter: die vor dem Konstrukt gebundenen Variablen, die sein Prüfausdruck
(der letzte Ausdruck des Rumpfs eines
try) und seine Guards lesen, in Quelltextreihenfolge (Variablen, die seine Klauselmuster binden, sind in jeder Klausel neu, keine Parameter); höchstens 4, die Einengung eines späteren wird vergessen. Jede mögliche Klausel behält die Fakten der Parameter nach ihrem Muster und Guard sowie ihren Wert (none()für eine Klausel, die nie abschließt); sie ist exakt, wie ein Funktionstyp bei der Aufrufauswahl, wenn eincaseodertryeinen Parameter oder ein Tupel von Parametern mit einem exakten Muster und Guard matcht oder einifeinen exakten Guard hat. - Ein Prüfausdruck, der ein Tupel von Variablen ist, engt jede Variable auf ihr
Element des gematchten Werts ein, und die Variable eines Tupelmusters wird an
denselben Wert gebunden wie das Element des Prüfausdrucks an ihrer Position
(
case {X, Y} of {A, b} when is_integer(A) -> ...engtXundYein). - Ein
try ... ofhängt von seinenof-Klauseln ab; seine catch-Klauseln können auf jeden Wert seines Rumpfs folgen, daher werden ihre vereinigten Werte in den Wert jeder Klausel hineinvereinigt (try X of 1 -> one; _ -> other catch _:_ -> error endist$try_of_operator(X :: 1) -> error | one; (X :: _) -> error | other). receiveundmaybe ... elsebleiben die Vereinigung ihrer Klauseln: Ihre Klauseln matchen eine Nachricht oder einen fehlgeschlagenen?=-Wert, den nichts außerhalb von ihnen benennen kann, daher könnte kein Lesezugriff zwischen ihnen auswählen.- Eine Klausel, deren Wert selbst abhängig ist (ein verschachteltes
caseoderifoder ein Lesezugriff auf eine abhängige Variable), trägt einen Funktionstyp pro Klausel davon über der Vereinigung beider Parameterlisten bei, die Eingaben geschnitten. Typen, die nie betreten werden können (eine Eingabenone()oder innerhalb der Eingaben eines früheren exakten Typs), werden verworfen, ebenso Parameter, deren Fakten in jedem Typ gleich sind; gleiche Eingaben werden zusammengeführt, und über 32 Typen hinaus werden die letzten zusammengeführt, wie bei Funktionstypen. Ein Konstrukt, dessen verbleibende Typen alle denselben Wert liefern oder das keinen Parameter einengt, ist seine einfache Vereinigung. - Jeder andere Verbraucher liest den gelöschten Fakt, die Vereinigung der Werte der Klauseln. Fakten, die die durchlaufene Funktion verlassen (Ergebnisse, Funktionstypen, Fun-Fakten), werden gelöscht; Vereinigungen behalten eine Abhängigkeit nur, wenn beide Seiten dieselbe haben.
- Eine an einen abhängigen Wert gebundene Variable behält ihn. Ein Lesezugriff
auf die Variable wählt die Klauseln aus, die die aktuellen Fakten ihrer
Parameter betreten (in der Reihenfolge der Aufrufauswahl; eine exakte
Klausel, deren Eingaben sie enthalten, beendet die Auswahl) und deren Werte
den eigenen eingeengten Fakt der Variable schneiden, und der Fakt des
Lesezugriffs ist die Vereinigung ihrer Werte
(
Y = case X of 1 -> one; _ -> other end, case X of 1 -> Y endliestone). - Eine Variable, die jede Klausel eines
caseoderifbindet, ist die Vereinigung ihrer Fakten am Ende der abschließenden Klauseln, abhängig von den Parametern des Konstrukts wie dessen Wert:case X of 1 -> Y = 5; _ -> Y = 6 end, Yinferiert(1) -> 5; (_) -> 6. Dieselbe Vereinigung gilt nachreceive,tryundmaybe, ohne Abhängigkeit. - Operatoren, Builtins, Tupel-, Listen-, Map- und Record-Konstruktionen sowie
Aufrufe von Batch-Funktionen (deren Funktionstypen wie bei der Aufrufauswahl
ausgewählt werden, die Argumente nicht erneut eingeengt) mit abhängigen
Operanden werden einmal pro Kombination der Klauseln der Operanden
ausgewertet, mit den Werten dieser Klauseln anstelle der Fakten der Operanden
und ihren Eingaben geschnitten: Lesezugriffe auf eine Variable nehmen
dieselbe Klausel, andere Operanden werden kombiniert, höchstens 16
Kombinationen (darüber hinaus ist die Verwendung die einfache Vereinigung).
R + 1mitR = case X of a -> 1; b -> 2 endinferiert(a) -> 2; (b) -> 3. - Das Einengen einer abhängigen Variable (ein Muster, ein Guard, eine
Verwendung) engt auch ihre Parameter auf die Vereinigung der Eingaben der
betretenen Klauseln ein, deren Werte den eingeengten Fakt schneiden, und
wiederum deren Parameter, 4 Ebenen tief: Nach
R = case X of a -> 1; b -> 2 endliestcase R of 1 -> X endXalsa. - Das abhängige Ergebnis einer Funktionsklausel teilt diese auf (siehe
Funktionstypen): Parameter, die ein Argument benennen, oder ein an dessen Wert
gebundener Name schneiden dessen Eingabe; andere werden verworfen (ein solcher
Typ ist nur exakt, wenn ihre Eingabe ein beliebiger Term ist).
nested_case(X, L)mitcase X of 1 -> case L of spanish -> uno; _ -> one end; _ -> other endinferiert(1, spanish) -> uno; (1, _) -> one; (_, _) -> other. --print-typesvermerkt einen abhängigen Wert als Funktionstyp, benannt nach seinem Konstrukt, jede Eingabe nach dem Namen ihres Parameters:case X of 1 -> one; _ -> other endendet aufend. % $case_of_operator(X :: 1) -> one; (X :: _) -> other; einifgibt$if_operatoraus, eintry ... of$try_of_operator.
Inferenzdomäne
semantic/types/lattice. Ein Fakt ist eine Menge von Werten, die eine Variable
oder ein Ergebnis haben kann. Fakten werden dort vereinigt, wo Kontrollfluss
zusammentrifft (Klauseln, Zweige), und zwischen den Runden einer rekursiven
Komponente aufgeweitet; jedes Budget unten weitet korrekt auf eine größere
Menge auf und weist nie ein Programm ab. Fakten werden als Erlang-Typen
ausgegeben, Kategorien mit 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, geht zur nächsten Schwelle: eine untere auf 1, dann 0, dann unbeschränkt; eine obere auf -1, dann unbeschränkt. Auch Schranken innerhalb von Containern werden aufgeweitet, wenn der vorherige Fakt an derselben Stelle ein Tupel (dieser Größe und mit diesem Tag), eine Map (mit diesen Schlüsseln) oder eine Liste hat |
| 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 aus Name und Größe eines sichtbaren Tupel-Records wird als der Record ausgegeben |
| Listen | [], [T], [T, ...], nonempty_improper_list(H, T), [1, a], [a, b | T] | Elemente werden vereinigt; [] mit einer nicht leeren Liste ergibt eine möglicherweise leere; unechte Listen vereinigen Köpfe und Enden. Listen aus zwei oder mehr bekannten Elementen behalten ihre Positionen (eine Notation von Clause, die Typsprache hat keine; Elemente mit | in Klammern ausgegeben): Positionslisten 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 kann fehlen) | 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 werden höchstens 4 Ebenen tief verschachtelt; ein tiefer liegender
Fakt wird zu
term(). - Eine Union verwirft ein exaktes Tupelmitglied, das ein anderes exaktes Tupel seiner Größe enthält (von zweien, die einander enthalten, bleibt das frühere), sodass eine Wertemenge eine einzige Union hat und Rekursionen über Bäume sich stabilisieren.
- Rekursive Komponenten vereinigen 8 Runden lang (Zyklen von bis zu 8 Funktionen konvergieren exakt) und weiten dann auf (Inferenz).
- Einengung (
Lattice::meet) ist der Schnitt von Fakten, die Werte, die beide enthalten (oder mehr, abernone()nur, wenn sie keine teilen). Ein leerer Schnitt bedeutet, dass der Pfad nicht laufen kann. - Fakten sind unveränderliche Knoten eines Graphen, daher speichert der Graph
das Ergebnis jeder Vereinigung zweier Fakten, jedes Schnitts und jeder
Inklusionsprüfung (
within) für den Rest des Batches: Fast alle wiederholen sich. Ein gespeichertes Ergebnis zählt weiterhin die dabei ausgeführten Vereinigungen für das kostenabhängige Aufgeben, sodass die Ergebnisse nicht von der Speicherung abhängen; ein Ergebnis, bei dessen Berechnung das Knotenbudget einen Fakt aufgeweitet hat, wird nicht gespeichert.
Bei -O2 mit Typspezialisierung verwendet das Lowering diese Fakten, um
Prüfungen zu entfernen (Spezialisierung);
Spezifikationen werden nie zu Fakten.
Inferenz-Cache
Module in den Verzeichnissen des Inferenz-Caches
können eine Kompilierung mit den Ergebnissen beginnen, die eine frühere
Kompilierung behalten hat: Zusammenfassungen, lokale Eingaben und
Ausdrucksfakten ihrer Funktionen. infer hält sie in jedem Durchlauf fest,
durchläuft nie eine Komponente, deren Mitglieder sie alle wiederverwenden, und
durchläuft jede andere Funktion wie gewohnt, wobei es die behaltenen
Zusammenfassungen wie jede andere liest.
- Wovon die Fakten eines Moduls abhängen. Exportierte Funktionen und
Funktionen, die ein
fun F/Abenennt, beginnen beiterm(); die Eingaben jeder anderen Funktion stammen von ihren Aufrufern, die im selben Modul liegen; ein Fakt benennt eine Funktion eines anderen Moduls nur über ein Atom: einen entfernten Aufruf,fun M:F/Aoderapply/2,3auf einem Atomfakt (und liest das Record-Layout eines anderen Moduls nur über einen nativen Record, den es qualifiziert oder importiert), und jeder Atomfakt stammt von einem Atom, das das Modul schreibt, oder von einem Fakt eines Moduls, das es erreicht. Die Fakten eines Moduls hängen also nur von seinem eigenen Code und vom Code der Batch-Module ab, die seine Atome benennen, transitiv. Der Schlüssel seiner behaltenen Ergebnisse deckt genau das ab: die Identität vonclau, den Fingerabdruck des Moduls (sein Quelltext, jede Datei, die es einbindet, Präprozessoroptionen und die Antwort eines Parse transform), vordefinierte Formen, Escript- und Bibliotheksstatus sowie dasselbe für jedes Batch-Modul, das seine Atome erreichen. Ein so erreichtes Modul ohne Fingerabdruck macht das Modul nicht cachebar. - Lebendigkeit. Die Lebendigkeit von Exporten entfernt nur Funktionen, und
ein entfernter Aufrufer engt nur die Eingaben der Funktionen ein, die er
aufgerufen hat: Mit mehr lebendigen Funktionen berechnete Fakten gelten
weiterhin. Behaltene Ergebnisse werden wiederverwendet, wenn sie jede jetzt
lebendige Funktion enthalten (sonst
results do not fit); ausgelassene Funktionen erhalten nichts. - Mischen. Behaltene und frische Ergebnisse können sich in einem Batch treffen: Die Fakten jedes Moduls gelten für jeden Lauf des Programms, unabhängig davon, was die Fakten eines anderen Moduls sagen, sodass ein frisches Modul, das eine behaltene Zusammenfassung liest (oder umgekehrt), korrekt bleibt. Eine rekursive Komponente mit Mitgliedern beider Arten wird als Ganzes durchlaufen.
- Präzision. Wiederverwendete Ergebnisse sind die der Kompilierung, die sie behalten hat. Die Ergebnisse eines frischen Moduls können trotzdem von einer Kompilierung ohne den Cache abweichen: Das Durchlaufbudget zahlt nicht mehr für wiederverwendete Module, und Graph-Identitäten, nach denen einige Union-Mitglieder geordnet sind, werden anders nummeriert.
--print-types
Gibt jedes Modul des Batches (in Eingabe-/Zielreihenfolge, Bibliotheksmodule danach) als Erlang-Quelltext aus (Ausgabe von Quelltext), zusammen mit dem, was die Typinferenz gefunden hat. Die Ausgabe geht auf stdout und ist für Menschen lesbar, 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) -> 1; (_) -> 2
mixed(X) ->
case X of
1 ->
1;
_ ->
2
end. % $case_of_operator(X :: 1) -> 1; (X :: _) -> 2
- Eine Zeile
%% modulenennt das Modul, seinen Quelltext, das Projektziel und ob deklarierte und inferierte Typen vollständig sind oder durch eine Grenze 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 sich beide vergleichen lassen. Die Eingaben sind die Erfolgsdomäne jedes Arguments (ausgehend vonterm()für exportierte Funktionen und die, diefun F/Abenennt, ausgehend von den vereinigten Argumenten der Aufrufer für andere Funktionen). Eine Funktion mit mehreren Funktionstypen gibt eine Signatur pro Typ aus, jede mit ihren eigenen Eingaben:f(integer()) -> integer(); (atom()) -> string()(semantic::types::function_source). Eine-specbleibt direkt vor der Funktion, durch eine Leerzeile von anderen Formen abgesetzt. - Der äußerste Ausdruck jeder Zeile (ein Rumpfausdruck oder der Prüfausdruck
eines
case), dessen Fakt mehr alsterm()aussagt, wird mit einem nachgestellten Kommentar% Typenach der Interpunktion der Zeile vermerkt (ein bekannter Typ verdeckt eine Argumentrelation, die nur bei einem sonst unbekannten Wert erscheint): höchstens ein Vermerk pro Zeile, keiner für die darin verschachtelten Ausdrücke, sodass die Ausgabe gültiges Erlang bleibt. Ein Rumpfausdruck über mehrere Zeilen trägt seinen Vermerk auf seiner letzten Zeile (end, % ...). Literale Terme (Literale sowie Tupel, Listen, konstruierte Maps und Bitstrings aus Literalen) werden nicht vermerkt; ein Match zeigt seinen Wert. Ein abhängiger Wert wird wie ein Funktionstyp seines Konstrukts ausgegeben (abhängige Fakten). - 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(ab 1 gezählt, 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 Zeile%% inferred:ausgeben sollte;today:, vorhanden, solange die Inferenz zurückbleibt, ist die, die es jetzt ausgibt.- Die Prüfung vergleicht die Ausgabe mit
today, wenn 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 und konstruierte Werte, Ergebnisse von Operatoren und Builtins, Container und ihre Teile, Funs und ihre Aufrufe, lokale Eingaben von Aufrufern, Ganzzahlvereinigungen und -bereiche, Argumentrelationen sowie Werte vontry/maybeab;narrowing.erlTyptests, Guards, Muster, Vergleichsbereiche, Disjunktionen und Einengungen, die nicht nach außen dringen dürfen;base_types.erleine Funktion pro Basis- und eingebautem Typ der Typsprache, deren-specaufgelöst werden muss und deren Rumpf einen solchen Wert erzeugt;clauses.erlFunktionstypen, Aufrufauswahl und Auswertung pro Aufruf innerhalb und jenseits ihrer Budgets;dependent.erlabhängige Fakten;trees.erlundparsers.erlRekursionen über Bäume und Formen erzeugter Parser;widening.erlZähler innerhalb von Containern, die aufgeweitet werden und sich stabilisieren.tests/fixtures/inference/contracts/enthält die Spezifikationsprüfungen (CTestinference_contracts).tests/fixtures/inference/budgets/walkers.erlist ein Compiler-Durchlauf in der üblichen Erlang-Form (ein großer Zustands-record, der durch wechselseitig rekursive Durchlauffunktionen gereicht wird, deren Ergebnisse Vereinigungen vieler records sind). CTestinference_walkers(tests/compiler/inference/walkers.py) hält seine Durchläufe, Arbeitsschritte, Gittervereinigungen sowie die Lösungen, Runden und Mitgliedsdurchläufe der Gruppe von Durchlauffunktionen innerhalb von Budgets, die etwa ein Viertel über den heutigen Werten liegen, und führt das Programm aus.
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 diese in einer Notation von Clause aus, fun((1) -> one; (_) -> other): Die Erlang-Typsyntax hat keinen überladenen Fun-Typ, und eine
Union von Fun-Typen bedeutet etwas anderes. Die Ganzzahlen einer Union werden
in Wertreihenfolge an der Stelle ihrer ersten Ganzzahl ausgegeben,
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