Parse-Transforms
Parse-Transforms verwenden
- Voraussetzung: Erlang/OTP 29 auf dem Host installiert, nur zum
Kompilieren von Modulen, die einen Transform verwenden
(OTP des Hosts).
--erl noneführt nie Host-Code aus: Module, die einen Transform brauchen, scheitern. - Einen Transform aufrufen im Quelltext mit
-compile({parse_transform, Module}).(oder innerhalb einer Liste-compile([...])) oder für jedes Modul des Befehls mit--parse-transform Module(wiederholbar; diese laufen zuerst, wieerlc +'{parse_transform,M}'). - Transform-Module werden als
.beam-Dateien auf den Transform-Pfaden gefunden, dann als Projektquellen, die bei jedem Build vom OTP des Hosts kompiliert werden, dann unter den Modulen von OTP (Transform-Module). - Vorkompilieren muss man einen Transform, der aus einer anderen
Bibliothek stammt (immer), oder einen Projekt-Transform, den der
Host-Compiler mit den Include-Verzeichnissen und Defines des Ziels nicht
bauen kann:
erlc -o deps/lib/ebin deps/lib/src/*.erlmit dem OTP des Hosts, danndeps/lib/ebinmit--transform-pathoder intransform_pathsangeben. Die.beammuss aus dem OTP-Release des Hosts stammen; eine.beamdort gewinnt gegenüber einer Quelle desselben Moduls. - Was ihn ausführt: Kompilieren,
--print-ast,--print-sourceund--print-abstr(das die transformierten Formen zeigt);--parse-check,--preprocess-checkund--print-pptun es nicht.
clau --erl "C:/Program Files/Erlang OTP/bin/erl.exe" --transform-path ebin -o app app.erl
Clause wendet {parse_transform, Module} so an, wie es erlc tut. Es
exportiert das geparste Modul als Formen im
Abstract Format von OTP,
führt das Transform-Modul auf einem auf dem Host installierten Erlang/OTP 29
aus, importiert die zurückgegebenen Formen als neue Syntax des Moduls und
kompiliert diese (compiler/src/transforms/). OTP wird nur gebraucht, während
ein Modul kompiliert wird, das einen Transform verwendet; erzeugte Programme
brauchen es nie.
Verhalten von OTP
Aus dem festgelegten lib/compiler/src/compile.erl (transform_module,
foldl_transform, maybe_strip_columns) und beobachtet unter OTP 29.1.1:
- Transforms laufen als erster Standarddurchlauf: nach
eppund dem Parsen, vorerl_lint, bevorerl_internalmodule_info/0,1undbehaviour_info/1hinzufügt. - Die Transform-Liste besteht aus jedem
{parse_transform, M}der Kompilieroptionen (erlc +'{parse_transform,m}'), dann aus denen der-compile-Attribute in Formreihenfolge. Ein zweimal genannter Transform läuft zweimal. Bevor der erste läuft, werden diese Optionen aus den Formen entfernt: Ein-compile-Attribut, das nur die Option enthält, verschwindet, eine Liste verliert das Element. Mmuss sich laden lassen (code:ensure_loaded/1) undparse_transform/2exportieren, sonst der Fehlerundefined parse transform 'M'(ohne Ort).M:parse_transform(Forms, Options)erhält die Formen und die Kompilieroptionen (nicht die-compile-Optionen des Quelltexts):{features, Enabled},report_warnings,report_errors,{cwd, Dir},{outdir, Dir},{i, Dir}pro Include-Verzeichnis,{d, Name[, Value]}pro Define, plus jedes als Option angegebene{parse_transform, M}.- Es gibt
Forms,{warning, Forms, Warnings}oder{error, Errors, Warnings}zurück, wobei jede Liste[{File, [{Location, Module, Descriptor}]}]ist, formatiert durchModule:format_error(Descriptor). Eine Ausnahme ist der Fehlererror in parse transform 'M': ...(compile:format_error/1von{parse_transform, M, {Class, Reason, Stack}}). Der nächste Transform erhält die Formen des vorigen; der erste Fehler beendet die Kette. - Gibt
M:parse_transform_info/0#{error_location => line}zurück, oder ist die Kompilieroption{error_location, line}gesetzt, werden Spalten aus den Formen entfernt, bevorMläuft, und aus den endgültigen Formen. - Formen tragen Annotationen
{Line, Column}.eppfügt zuerst{attribute, {1,1}, file, {File, 1}}hinzu, dasselbe für jede eingebundene Datei, wenn sie beginnt, und{attribute, {L,1}, file, {File, L}}, wenn die einbindende Datei in ZeileLfortfährt, der Zeile nach ihrem-include. Makrodefinitionen und andere Direktiven hinterlassen keine Form. Die Liste endet mit{eof, Location}, dem Ende der Hauptdatei. Präprozessor- und Parse-Fehler bleiben als Formen{error, E}in der Liste, underl_lintmeldet sie nach den Transforms. - Formen von OTP 29, die
absform.mdnicht aufführt:{attribute, A, native_record, {Name, Fields}}; Record-Namen{Module, Name}(qualifiziert) und[](#_) in Ausdrückenrecord/record_field;{type, A, record, [{tuple, A, [Module, Name]} | Fields]}für qualifizierte Record-Typen;{zip, A, Qualifiers}als Qualifier von Comprehensions;{mc, ...},m_generateund die_strict-Generatoren. erlc +to_abstrschreibt die Formen nach den Transforms (underl_lint) nachFile.abstr, ein~p.-Term pro Form;erlc File.abstr(compile:file(F, [from_abstr])) kompiliert eine solche Datei ohne Präprozessor oder Parser.
Pipeline von Clause
- Das Modul präprozessieren und parsen. Präprozessor- oder Parse-Fehler beenden hier; Transforms laufen nicht auf einem Modul mit Fehlern (abweichend von OTP, das die Fehler als Formen weitergibt).
- Die Transforms in der Reihenfolge von OTP sammeln: Optionen
--parse-transformund dieparse_transformsdes Projekts, dann-compile-Attribute. Ein Modul ohne Transforms überspringt den Rest und startet OTP nie. - Diese Optionen aus den Formen entfernen und sie exportieren: Annotationen
{Line, Column}, wieerl_parsesie vergibt, diefile-Attribute voneppundeof. Die vordefinierten Funktionen sind noch nicht Teil der Formen. - Den Loader einmal für das Modul ausführen.
- Die zurückgegebenen Formen importieren, indem sie in Tokens umgewandelt
werden, die an ihren Annotationen verortet sind, und mit dem Parser von
Clause geparst werden; ein
file-Attribut wechselt die Datei der folgenden Formen wie inerl_lint. - Die vordefinierten Funktionen hinzufügen, dann wie üblich linten, analysieren und kompilieren.
Die Aktionen, die Transforms ausführen, stehen unter Parse-Transforms verwenden.
Austausch
clauund der Loader tauschen Dateien im External Term Format aus (term_to_binary, Version 131, unkomprimiert): exakte Gleitkommazahlen, UTF-8-Atome, große Ganzzahlen und kein Parsen von Text in Erlang. Das Format selbst (Tags, Layouts, Grenzprüfungen) liegt einmal inabi/include/clause/abi/external_term.hpp, unabhängig von jeder Termdarstellung;term_to_binaryundbinary_to_termder Runtime teilen es (External Term Format). Das Termmodell des Compilers von Clause umfasst Atome, Ganzzahlen, Gleitkommazahlen, echte und unechte Listen, Tupel, Maps und Bitstrings; Pids, Ports, Referenzen und Funs werden mit einer Diagnose abgewiesen..abstr-Text ist die Syntax vonfile:consult/1: ein Term pro Form, jeder endet mit..--print-abstrgibt die Formen jedes Moduls nach den Transforms in dieser Syntax auf stdout aus, eine Form pro Zeile; eine.abstr-Eingabedatei wird wieerlc File.abstrkompiliert. Clause schreibt kanonischen Text, nicht das~p-Layout von OTP; es kommt auf gleiche Terme an. Die Formen gleichenepp:parse_file(F, [{location, {1, 1}}])für jedes Konstrukt vontests/fixtures/transforms/abstract/, einschließlich derfile-Attribute vonepp(ein explizites-filewird mit[{generated, true}, {location, L}]annotiert) und eineseof, gezählt ab dem letzten-fileder Hauptdatei.
Formen importieren
Zurückgegebene Formen und .abstr-Eingaben (positional oder Projekt-sources)
werden zu Tokens, die an ihren Annotationen verortet sind, und durchlaufen den
Parser von Clause, sodass sie dieselben Syntaxprüfungen wie Quelltext erhalten;
die Operatorrangfolge entscheidet, wo Klammern ergänzt werden. {Line, Column},
ein bloßes Line (angezeigt als file:line:) und Property-Listen mit
location und file werden akzeptiert; generated wird ignoriert. Zeile 0,
mit der der qlc-Transform von OTP erzeugten Code versieht, übernimmt die
Position des zuvor importierten Knotens. Ein file-Attribut wechselt die Datei
der folgenden Formen; existiert diese Datei,
behält importierter Code seine Debug-Zeilen und Quelltextkommentare.
Fehlerhafte Formen sind Fehler an ihrer Annotation (unknown expression bogus,
expected {Name, Arity}, invalid annotation), und das Modul scheitert; die
Formen {error, _}/{warning, _} von OTP werden mit ihrem Deskriptor
gemeldet, dessen Formatierung OTP braucht. Ein exportiertes Modul zu
importieren und erneut zu exportieren ergibt dieselben Formen.
OTP des Hosts
erlwird in dieser Reihenfolge gefunden:--erl PATH,erlausPATH, dann unter Windows%ProgramFiles%\Erlang OTP\bin\erl.exe. Keines zu finden, wenn ein Modul einen Transform braucht, ist ein Fehler, der den Transform und das Modul nennt; es gibt keinen Rückfall, der Transforms überspringt.- Der Host muss OTP 29 ausführen (
erlang:system_info(otp_release)); ein anderes Release ist ein Fehler. - Der Loader läuft als
erl -noshell -noinput -pa Dir ... -eval ...(Verzeichnisse des Codepfads absolut gemacht) mit einer Zeitgrenze von 600 s. Sein stdout und stderr (einschließlich derio:format-Ausgabe des Transforms) werden nach seinem Ende in das stderr vonclaukopiert. Ein Loader, der endet, ohne eine Antwort zu schreiben, die Zeit überschreitet oder nicht starten kann, ist ein Fehler. - Ein Transform führt beliebigen Code mit den Rechten des Nutzers aus, genau
wie unter
erlc.
Loader
compiler/src/transforms/loader/clause_transform_loader.erl gehört dem Projekt
und ist in clau eingebettet. Jeder Lauf schreibt ihn und die Anfrage in ein
frisches temporäres Verzeichnis, clause-transform-* unter <build_dir>/tmp
(Build-Verzeichnis) oder, für positionale
Eingaben ohne --build-dir, im temporären Verzeichnis des Systems, das nach
dem Lauf entfernt wird; erl kompiliert und lädt den Loader im Speicher. Die
Anfrage enthält die Formen, die Optionen, die Transform-Liste, den Dateinamen
des Moduls und die zu bauenden Transform-Quellen des Projekts. Mit einem
Build-Verzeichnis werden die Antwort und die Ausgabe des Loaders im
Build-Cache behalten und wiederverwendet, solange
die Anfrage und alles, wovon sie abhängt, unverändert sind; die Liste der
Sticky-Module des Hosts wird auf dieselbe Weise behalten. Der Loader:
- prüft das OTP-Release;
- kompiliert die angefragten Transform-Quellen des Projekts im Speicher und lädt sie;
- für jeden Transform: lädt ihn, entfernt Spalten, wie es
parse_transform_info/0verlangt, ruftparse_transform/2auf und formatiert Warnungen und Fehler mitModule:format_error/1zu{File, Location, Text}; - schreibt
{ok, Forms, Warnings}oder{error, Errors, Warnings}und hält an.
Transform-Module
Ein Transform-Modul wird in dieser Reihenfolge gesucht:
- Vorkompilierte
.beam-Dateien in den Verzeichnissen von--transform-path DIR(wiederholbar) und der Projektoptiontransform_paths, die vor denen von OTP in den Codepfad des Loaders kommen. Das ist der einzige Weg, einen Transform zu verwenden, der aus einer abhängigen Bibliothek stammt: Diese Bibliothek zuerst mit dem OTP des Hosts bauen (zum Beispielerlc -o deps/lib/ebin deps/lib/src/*.erl); Clause baut nie Abhängigkeiten. Die.beammuss aus dem OTP-Hauptrelease des Hosts stammen. - Die Quellen des aktuellen Projekts: eine
Module.erlim Batch oder in seinensource_search_paths, bei jedem Lauf vom Loader im Speicher mit den Include-Verzeichnissen und Defines des Ziels kompiliert, zusammen mit den Projektmodulen, die er per Namen aufruft (auf dieselbe Weise gefunden). Die Suche übergeht Quellen, dieignore_dirsoderignore_modulesauslassen, wie es die Modulsuche tut. Clause sucht diese Quellen einmal pro Batch und behält sie im Speicher; der Loader kompiliert sie dennoch für jedes Modul, auf dem der Transform läuft, daher lohnt es sich, einen großen Transform vorzukompilieren, um diese Zeit zu sparen. Module, die der Host aus seinen Sticky-Verzeichnissen lädt (kernel,stdlib,compiler, etwams_transformoderqlc_pt, wenn die Quellen von OTP auf einem Suchpfad liegen), lassen sich nicht neu laden, daher werden die eigenen Module des Hosts verwendet: Ihre Quellen und die Module, die nur sie aufrufen, werden weder gesucht noch kompiliert. Clause fragt daserldes Hosts einmal pro Aufruf nach diesen Modulen. - Die eigenen Module von OTP (
ms_transform,qlc) aus dem Codepfad des Hosts. OTP lieferterl_id_transnur als Beispielquelle, nicht als Modul. Ihre Header werden wie jedesinclude_libaufgelöst (Präprozessor): die Anwendung zuordnen, wie in--app-dir "stdlib=C:/Program Files/Erlang OTP/lib/stdlib-8.1"fürinclude_lib("stdlib/include/ms_transform.hrl"), oder den Transform direkt mit-compile({parse_transform, ms_transform})nennen.
Das Kompilieren eines Projekt-Transforms kann scheitern, wo sein echter Build
gelingt: Header von Abhängigkeiten, ein Transform des Transforms oder Makros,
die nur ein anderes Build-Werkzeug definiert. Clause meldet dann die Meldungen
des Compilers zur Quelle des Transforms mit dem Hinweis, ihn mit dem OTP des
Hosts zu einer .beam vorzukompilieren und ihr Verzeichnis mit
--transform-path zu übergeben. Ein Projekt-Transform wird von Clause auch als
gewöhnliches Modul kompiliert, wenn er Teil des Batches ist; seine eigenen
Aufrufe (erl_syntax, erl_parse, ...) können dann Bibliotheksmodule
brauchen, die Clause nicht mitliefert, daher Transforms aus den sources eines
Ziels heraushalten, wenn nur der Transform sie braucht.
Diagnosen
| Situation | Meldung |
|---|---|
| Kein OTP auf dem Host | File: parse transform 'M' needs Erlang/OTP 29 on the host; pass --erl |
--erl nennt keine ausführbare Datei | File: erl not found: PATH |
| Falsches OTP-Release | parse transforms need Erlang/OTP 29 on the host, found N |
| Transform nicht gefunden | File: undefined parse transform 'M' (Text von OTP) |
| Transform hat ausgelöst | File: error in parse transform 'M': ... (Text von OTP) |
| Warnung oder Fehler des Transforms | File:Line:Column: Text wie zurückgegeben |
| Transform-Quelle kompiliert nicht | ihre Meldungen, dann der Hinweis zum Vorkompilieren |
| Ein Transform-Pfad existiert nicht | File: parse transform path is not a directory: DIR |
Eine .beam auf einem Transform-Pfad lädt nicht | File: cannot load parse transform 'M' from PATH: Reason |
| Loader gescheitert, Zeit überschritten oder keine Antwort geschrieben | parse transform loader failed: ... |
| Ungültige zurückgegebene Formen | die Diagnose des Parsers am Ort der Form |
Optionen
| CLI | Projekt ([targets.options]) | Bedeutung |
|---|---|---|
--parse-transform M | parse_transforms = ["m"] | M auf jedes Modul anwenden, vor dessen eigenen -compile-Transforms (die der CLI zuerst) |
--transform-path DIR | transform_paths = ["deps/*/ebin"] | Codepfad für vorkompilierte Transform-.beam-Dateien; Muster wie in source_search_paths |
--erl PATH oder none | — | erl-Datei des Hosts; none führt nie Host-Code aus |
--print-abstr | — | Die Formen nach den Transforms ausgeben, statt zu kompilieren |
Ein Projekt, das einen eigenen Transform und einen vorkompilierten aus einer Abhängigkeit verwendet:
[[targets]]
name = "app"
sources = ["src/*.erl"]
[targets.options]
source_search_paths = ["transforms"] # transforms/pt_rewrite.erl, compiled for each build
parse_transforms = ["pt_dep"] # applied to every module of the target
transform_paths = ["deps/*/ebin"] # deps/ptdep/ebin/pt_dep.beam, built with erlc first
Clause