Parse transforms
Utiliser les parse transforms
- Prérequis : Erlang/OTP 29 installé sur l'hôte, uniquement pour compiler
les modules qui utilisent une transformation (OTP de l'hôte).
--erl nonen'exécute jamais de code de l'hôte : les modules qui ont besoin d'une transformation échouent. - Invoquer une transformation dans le source avec
-compile({parse_transform, Module}).(ou dans une liste-compile([...])), ou pour chaque module de la commande avec--parse-transform Module(répétable ; celles-ci s'exécutent en premier, commeerlc +'{parse_transform,M}'). - Les modules de transformation sont trouvés sous forme de fichiers
.beamdans les chemins de transformations, puis sous forme de sources du projet compilés par l'OTP de l'hôte à chaque build, puis parmi les modules d'OTP (modules de transformation). - Précompiler une transformation qui provient d'une autre bibliothèque
(toujours), ou une transformation du projet que le compilateur de l'hôte ne
peut pas construire avec les répertoires d'inclusion et les définitions de
la cible :
erlc -o deps/lib/ebin deps/lib/src/*.erlavec l'OTP de l'hôte, puis nommerdeps/lib/ebinavec--transform-pathou danstransform_paths. Le.beamdoit provenir de la version d'OTP de l'hôte ; un.beamà cet endroit l'emporte sur un source du même module. - Ce qui l'exécute : la compilation,
--print-ast,--print-sourceet--print-abstr(qui montre les formes transformées) ;--parse-check,--preprocess-checket--print-ppne l'exécutent pas.
clau --erl "C:/Program Files/Erlang OTP/bin/erl.exe" --transform-path ebin -o app app.erl
Clause applique {parse_transform, Module} comme le fait erlc. Il exporte le
module analysé sous forme de formes du
format abstrait d'OTP,
exécute le module de transformation sur un Erlang/OTP 29 installé sur l'hôte,
importe les formes renvoyées comme nouvelle syntaxe du module et compile
celle-ci (compiler/src/transforms/). OTP n'est nécessaire que pendant la
compilation d'un module qui utilise une transformation ; les programmes
générés n'en ont jamais besoin.
Comportement d'OTP
D'après le lib/compiler/src/compile.erl épinglé (transform_module,
foldl_transform, maybe_strip_columns) et observé sous OTP 29.1.1 :
- Les transformations s'exécutent comme première passe standard : après
eppet l'analyse syntaxique, avanterl_lint, avant qu'erl_internaln'ajoutemodule_info/0,1etbehaviour_info/1. - La liste des transformations est chaque
{parse_transform, M}des options de compilation (erlc +'{parse_transform,m}'), puis des attributs-compiledans l'ordre des formes. Une transformation nommée deux fois s'exécute deux fois. Avant l'exécution de la première, ces options sont retirées des formes : un attribut-compilene contenant que l'option disparaît, une liste perd l'élément. Mdoit se charger (code:ensure_loaded/1) et exporterparse_transform/2, sinon l'erreurundefined parse transform 'M'(sans emplacement).M:parse_transform(Forms, Options)reçoit les formes et les options de compilation (pas les options-compiledu source) :{features, Enabled},report_warnings,report_errors,{cwd, Dir},{outdir, Dir},{i, Dir}par répertoire d'inclusion,{d, Name[, Value]}par définition, plus tout{parse_transform, M}donné comme option.- Il renvoie
Forms,{warning, Forms, Warnings}ou{error, Errors, Warnings}, où chaque liste est[{File, [{Location, Module, Descriptor}]}]formatée parModule:format_error(Descriptor). Une exception donne l'erreurerror in parse transform 'M': ...(compile:format_error/1de{parse_transform, M, {Class, Reason, Stack}}). La transformation suivante reçoit les formes de la précédente ; la première erreur arrête la chaîne. M:parse_transform_info/0renvoyant#{error_location => line}, ou l'option de compilation{error_location, line}, retire les colonnes des formes avant l'exécution deMet des formes finales.- Les formes portent des annotations
{Line, Column}.eppajoute d'abord{attribute, {1,1}, file, {File, 1}}, la même chose pour chaque fichier inclus lorsqu'il commence, et{attribute, {L,1}, file, {File, L}}lorsque le fichier qui inclut reprend à la ligneL, celle qui suit son-include. Les définitions de macros et les autres directives ne laissent aucune forme. La liste se termine par{eof, Location}, la fin du fichier principal. Les erreurs du préprocesseur et de l'analyse syntaxique restent dans la liste sous forme de formes{error, E}eterl_lintles signale après les transformations. - Formes d'OTP 29 que
absform.mdne liste pas :{attribute, A, native_record, {Name, Fields}}; noms de records{Module, Name}(qualifiés) et[](#_) dans les expressionsrecord/record_field;{type, A, record, [{tuple, A, [Module, Name]} | Fields]}pour les types de records qualifiés ; qualificateurs de comprehension{zip, A, Qualifiers};{mc, ...},m_generateet les générateurs_strict. erlc +to_abstrécrit les formes après les transformations (eterl_lint) dansFile.abstr, un terme~p.par forme ;erlc File.abstr(compile:file(F, [from_abstr])) compile un tel fichier sans préprocesseur ni analyseur.
Chaîne de Clause
- Prétraiter et analyser le module. Les erreurs du préprocesseur ou de l'analyse syntaxique arrêtent ici ; les transformations ne s'exécutent pas sur un module comportant des erreurs (diffère d'OTP, qui transmet les erreurs sous forme de formes).
- Collecter les transformations dans l'ordre d'OTP : les options
--parse-transformet lesparse_transformsdu projet, puis les attributs-compile. Un module qui n'en a aucune saute le reste et ne démarre jamais OTP. - Retirer ces options des formes et exporter celles-ci : annotations
{Line, Column}telles qu'erl_parseles attribue, attributsfiled'epp, eteof. Les fonctions prédéfinies ne font pas encore partie des formes. - Exécuter le chargeur une fois pour le module.
- Importer les formes renvoyées en les transformant en jetons situés à leurs
annotations et en les analysant avec l'analyseur de Clause ; un attribut
filechange le fichier des formes suivantes comme danserl_lint. - Ajouter les fonctions prédéfinies, puis effectuer le lint, l'analyse et la compilation comme d'habitude.
Les actions qui exécutent les transformations sont listées sous utiliser les parse transforms.
Échange
clauet le chargeur échangent des fichiers au format External Term Format (term_to_binary, version 131, non compressé) : flottants exacts, atomes UTF-8, grands entiers et aucune analyse de texte en Erlang. Le format lui-même (étiquettes, dispositions, vérifications de bornes) vit une seule fois dansabi/include/clause/abi/external_term.hpp, indépendamment de toute représentation des termes ; lesterm_to_binaryetbinary_to_termdu runtime le partagent (format des termes externes). Le modèle de termes du compilateur de Clause contient les atomes, les entiers, les flottants, les listes propres et impropres, les tuples, les maps et les bitstrings ; les pids, ports, références et funs sont rejetés avec un diagnostic.- Le texte
.abstrsuit la syntaxe defile:consult/1: un terme par forme, chacun se terminant par..--print-abstraffiche sur stdout les formes de chaque module après les transformations dans cette syntaxe, une forme par ligne ; un fichier d'entrée.abstrse compile commeerlc File.abstr. Clause écrit un texte canonique, pas la mise en page~pd'OTP ; ce qui compte, ce sont des termes égaux. Les formes sont égales àepp:parse_file(F, [{location, {1, 1}}])pour chaque construction detests/fixtures/transforms/abstract/, y compris les attributsfiled'epp(un-fileexplicite est annoté[{generated, true}, {location, L}]) et uneofcompté depuis le dernier-filedu fichier principal.
Importer des formes
Les formes renvoyées, et les entrées .abstr (positionnelles ou sources du
projet), deviennent des jetons situés à leurs annotations et passent par
l'analyseur de Clause, de sorte qu'elles subissent les mêmes vérifications
syntaxiques que le source ; la précédence des opérateurs décide où des
parenthèses sont ajoutées. {Line, Column}, un Line seul (affiché
file:line:) et les listes de propriétés avec location et file sont
acceptés ; generated est ignoré. La ligne 0, que la transformation qlc
d'OTP attribue au code généré, prend l'emplacement du nœud importé avant elle.
Un attribut de fichier change le fichier
des formes suivantes ; lorsque ce fichier existe, le code importé garde ses
lignes de débogage et ses commentaires de source. Les formes mal formées sont
des erreurs à leur annotation (unknown expression bogus,
expected {Name, Arity}, invalid annotation) et le module échoue ; les
formes {error, _}/{warning, _} d'OTP sont signalées avec leur descripteur,
dont le formatage exige OTP. Importer un module exporté et l'exporter de
nouveau donne les mêmes formes.
OTP de l'hôte
erlest cherché dans cet ordre :--erl PATH,erldans lePATH, puis sous Windows%ProgramFiles%\Erlang OTP\bin\erl.exe. Ne pas en trouver lorsqu'un module a besoin d'une transformation est une erreur qui nomme la transformation et le module ; il n'existe aucun repli qui saute les transformations.- L'hôte doit exécuter OTP 29 (
erlang:system_info(otp_release)) ; une autre version est une erreur. - Le chargeur s'exécute sous la forme
erl -noshell -noinput -pa Dir ... -eval ...(répertoires du chemin de code rendus absolus) avec une limite de temps de 600 s. Sa sortie standard et sa sortie d'erreur (y compris la sortieio:formatde la transformation) sont recopiées sur la sortie d'erreur declauaprès sa fin. Un chargeur qui se termine sans écrire de réponse, dépasse le délai ou ne peut pas démarrer est une erreur. - Une transformation exécute du code arbitraire avec les droits de
l'utilisateur, exactement comme elle le ferait sous
erlc.
Chargeur
compiler/src/transforms/loader/clause_transform_loader.erl appartient au
projet et est embarqué dans clau. Chaque exécution l'écrit, ainsi que la
requête, dans un nouveau répertoire temporaire, clause-transform-* sous
<build_dir>/tmp (répertoire de build) ou,
pour les entrées positionnelles sans --build-dir, le répertoire temporaire du
système, supprimé après l'exécution ; erl compile et charge le chargeur en
mémoire. La requête contient les formes, les options, la liste des
transformations, le nom de fichier du module et les sources de
transformations du projet à construire. Avec un répertoire de build, la
réponse et la sortie du chargeur sont gardées dans le
cache de build et réutilisées tant que la requête
et tout ce dont elle dépend restent inchangés ; la liste des modules collants
(sticky) de l'hôte est gardée de la même façon. Le chargeur :
- vérifie la version d'OTP ;
- compile en mémoire les sources de transformations du projet demandées et les charge ;
- pour chaque transformation : la charge, retire les colonnes comme le
demande
parse_transform_info/0, appelleparse_transform/2et formate les avertissements et les erreurs en{File, Location, Text}avecModule:format_error/1; - écrit
{ok, Forms, Warnings}ou{error, Errors, Warnings}et s'arrête.
Modules de transformation
Un module de transformation est recherché dans cet ordre :
- Fichiers
.beamprécompilés dans les répertoires de--transform-path DIR(répétable) et de l'option de projettransform_paths, qui sont placés dans le chemin de code du chargeur avant ceux d'OTP. C'est la seule façon d'utiliser une transformation qui provient d'une bibliothèque de dépendance : construire d'abord cette bibliothèque avec l'OTP de l'hôte (par exempleerlc -o deps/lib/ebin deps/lib/src/*.erl) ; Clause ne construit jamais les dépendances. Le.beamdoit provenir de la version majeure d'OTP de l'hôte. - Les sources du projet courant : un
Module.erldans le lot ou sessource_search_paths, compilé en mémoire par le chargeur avec les répertoires d'inclusion et les définitions de la cible à chaque exécution, avec les modules du projet qu'il appelle par leur nom (trouvés de la même façon). La recherche ignore les sources queignore_dirsouignore_moduleslaissent de côté, comme le fait la recherche de modules. Clause cherche ces sources une fois par lot et les garde en mémoire ; le chargeur les compile tout de même pour chaque module sur lequel la transformation s'exécute, donc précompiler une grosse transformation fait gagner ce temps. Les modules que l'hôte charge depuis ses répertoires collants (kernel,stdlib,compiler, commems_transformouqlc_ptlorsque les sources d'OTP sont sur un chemin de recherche) ne peuvent pas être rechargés, donc les modules propres à l'hôte sont utilisés : leurs sources, et les modules qu'eux seuls appellent, ne sont ni recherchés ni compilés. Clause demande ces modules à l'erlde l'hôte une fois par invocation. - Les modules propres à OTP (
ms_transform,qlc) depuis le chemin de code de l'hôte. OTP ne livreerl_id_transque comme source d'exemple, pas comme module. Leurs en-têtes se résolvent comme toutinclude_lib(préprocesseur) : associer l'application, comme dans--app-dir "stdlib=C:/Program Files/Erlang OTP/lib/stdlib-8.1"pourinclude_lib("stdlib/include/ms_transform.hrl"), ou nommer directement la transformation avec-compile({parse_transform, ms_transform}).
Compiler une transformation du projet peut échouer là où son vrai build
réussit : en-têtes de dépendances, transformation de la transformation, ou
macros que seul un autre outil de build définit. Clause signale alors les
messages du compilateur sur le source de la transformation, avec l'indication
de la précompiler en .beam avec l'OTP de l'hôte et de passer son répertoire
avec --transform-path. Une transformation du projet est aussi compilée par
Clause comme module ordinaire lorsqu'elle fait partie du lot ; ses propres
appels (erl_syntax, erl_parse, ...) peuvent alors exiger des modules de
bibliothèque que Clause ne livre pas, donc garder les transformations hors des
sources d'une cible lorsque seule la transformation en a besoin.
Diagnostics
| Situation | Message |
|---|---|
| Pas d'OTP sur l'hôte | File: parse transform 'M' needs Erlang/OTP 29 on the host; pass --erl |
--erl ne nomme aucun exécutable | File: erl not found: PATH |
| Mauvaise version d'OTP | parse transforms need Erlang/OTP 29 on the host, found N |
| Transformation introuvable | File: undefined parse transform 'M' (texte d'OTP) |
| La transformation a levé une exception | File: error in parse transform 'M': ... (texte d'OTP) |
| Avertissement ou erreur de la transformation | File:Line:Column: Text tel que renvoyé |
| Le source de la transformation ne compile pas | ses messages, puis l'indication de précompilation |
| Un chemin de transformation n'existe pas | File: parse transform path is not a directory: DIR |
Un .beam d'un chemin de transformation ne se charge pas | File: cannot load parse transform 'M' from PATH: Reason |
| Le chargeur a échoué, dépassé le délai ou n'a écrit aucune réponse | parse transform loader failed: ... |
| Formes renvoyées invalides | le diagnostic de l'analyseur à l'emplacement de la forme |
Options
| CLI | Projet ([targets.options]) | Signification |
|---|---|---|
--parse-transform M | parse_transforms = ["m"] | Applique M à chaque module, avant ses propres transformations -compile (celles de la CLI d'abord) |
--transform-path DIR | transform_paths = ["deps/*/ebin"] | Chemin de code pour les fichiers .beam de transformations précompilées ; motifs comme dans source_search_paths |
--erl PATH ou none | — | Exécutable erl de l'hôte ; none n'exécute jamais de code de l'hôte |
--print-abstr | — | Affiche les formes après les transformations au lieu de compiler |
Un projet qui utilise sa propre transformation et une transformation précompilée provenant d'une dépendance :
[[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