Parse transforms
Uso de parse transforms
- Requisito: Erlang/OTP 29 instalado en el anfitrión, solo para compilar
módulos que usan una transformación (OTP del anfitrión).
--erl nonenunca ejecuta código del anfitrión: los módulos que necesitan una transformación fallan. - Invocar una transformación en el código fuente con
-compile({parse_transform, Module}).(o dentro de una lista-compile([...])), o para todos los módulos de la orden con--parse-transform Module(repetible; estas se ejecutan primero, comoerlc +'{parse_transform,M}'). - Los módulos de transformación se buscan como archivos
.beamen las rutas de transformaciones, después como fuentes del proyecto compiladas por el OTP del anfitrión en cada compilación, y después entre los módulos de OTP (módulos de transformación). - Precompilar una transformación que procede de otra biblioteca (siempre),
o una transformación del proyecto que el compilador del anfitrión no puede
construir con los directorios de include y las definiciones del destino:
erlc -o deps/lib/ebin deps/lib/src/*.erlcon el OTP del anfitrión, y después nombrardeps/lib/ebincon--transform-patho entransform_paths. El.beamdebe proceder de la versión de OTP del anfitrión; un.beamallí prevalece sobre un fuente del mismo módulo. - Qué la ejecuta: la compilación,
--print-ast,--print-sourcey--print-abstr(que muestra las formas transformadas);--parse-check,--preprocess-checky--print-ppno.
clau --erl "C:/Program Files/Erlang OTP/bin/erl.exe" --transform-path ebin -o app app.erl
Clause aplica {parse_transform, Module} como lo hace erlc. Exporta el
módulo analizado como formas del
formato abstracto de OTP,
ejecuta el módulo de transformación en un Erlang/OTP 29 instalado en el
anfitrión, importa las formas devueltas como la nueva sintaxis del módulo y
compila eso (compiler/src/transforms/). OTP solo se necesita mientras se
compila un módulo que usa una transformación; los programas generados nunca lo
necesitan.
Comportamiento de OTP
A partir de lib/compiler/src/compile.erl fijado (transform_module,
foldl_transform, maybe_strip_columns) y observado con OTP 29.1.1:
- Las transformaciones se ejecutan como la primera pasada estándar: después de
eppy del análisis sintáctico, antes deerl_linty antes de queerl_internalañadamodule_info/0,1ybehaviour_info/1. - La lista de transformaciones es cada
{parse_transform, M}de las opciones de compilación (erlc +'{parse_transform,m}') y después de los atributos-compileen orden de formas. Una transformación nombrada dos veces se ejecuta dos veces. Antes de que se ejecute la primera, esas opciones se eliminan de las formas: un atributo-compileque solo contiene la opción desaparece, y una lista pierde el elemento. Mdebe cargarse (code:ensure_loaded/1) y exportarparse_transform/2; si no, se produce el errorundefined parse transform 'M'(sin ubicación).M:parse_transform(Forms, Options)recibe las formas y las opciones de compilación (no las opciones-compiledel fuente):{features, Enabled},report_warnings,report_errors,{cwd, Dir},{outdir, Dir},{i, Dir}por directorio de include,{d, Name[, Value]}por definición, más cualquier{parse_transform, M}dado como opción.- Devuelve
Forms,{warning, Forms, Warnings}o{error, Errors, Warnings}, donde cada lista es[{File, [{Location, Module, Descriptor}]}]formateada porModule:format_error(Descriptor). Una excepción es el errorerror in parse transform 'M': ...(compile:format_error/1de{parse_transform, M, {Class, Reason, Stack}}). La siguiente transformación recibe las formas de la anterior; el primer error detiene la cadena. M:parse_transform_info/0que devuelve#{error_location => line}, o la opción de compilación{error_location, line}, elimina las columnas de las formas antes de que se ejecuteMy de las formas finales.- Las formas llevan anotaciones
{Line, Column}.eppañade primero{attribute, {1,1}, file, {File, 1}}, lo mismo para cada archivo incluido cuando empieza, y{attribute, {L,1}, file, {File, L}}cuando el archivo que incluye continúa en la líneaL, la línea siguiente a su-include. Las definiciones de macros y otras directivas no dejan ninguna forma. La lista termina con{eof, Location}, el final del archivo principal. Los errores del preprocesador y del análisis sintáctico permanecen en la lista como formas{error, E}yerl_lintlos informa después de las transformaciones. - Formas de OTP 29 que
absform.mdno enumera:{attribute, A, native_record, {Name, Fields}}; nombres de record{Module, Name}(cualificados) y[](#_) en expresionesrecord/record_field;{type, A, record, [{tuple, A, [Module, Name]} | Fields]}para tipos de record cualificados; calificadores de comprehension{zip, A, Qualifiers};{mc, ...},m_generatey los generadores_strict. erlc +to_abstrescribe las formas después de las transformaciones (y deerl_lint) enFile.abstr, un término~p.por forma;erlc File.abstr(compile:file(F, [from_abstr])) compila un archivo así sin preprocesador ni parser.
Pipeline de Clause
- Preprocesar y analizar el módulo. Los errores del preprocesador o del análisis sintáctico se detienen aquí; las transformaciones no se ejecutan en un módulo con errores (difiere de OTP, que transmite los errores como formas).
- Reunir las transformaciones en el orden de OTP: las opciones
--parse-transformy losparse_transformsdel proyecto, y después los atributos-compile. Un módulo sin ninguna se salta el resto y nunca inicia OTP. - Eliminar esas opciones de las formas y exportarlas: anotaciones
{Line, Column}tal como las asignaerl_parse, los atributosfiledeeppyeof. Las funciones predefinidas todavía no forman parte de las formas. - Ejecutar el cargador una vez para el módulo.
- Importar las formas devueltas convirtiéndolas en tokens ubicados en sus
anotaciones y analizándolas con el parser de Clause; un atributo
filecambia el archivo de las formas siguientes como enerl_lint. - Añadir las funciones predefinidas y después analizar con lint, analizar y compilar como de costumbre.
Las acciones que ejecutan transformaciones se enumeran en uso de parse transforms.
Intercambio
clauy el cargador intercambian archivos en External Term Format (term_to_binary, versión 131, sin comprimir): floats exactos, átomos UTF-8, enteros grandes y ningún análisis de texto en Erlang. El formato en sí (etiquetas, disposiciones, comprobaciones de límites) reside una sola vez enabi/include/clause/abi/external_term.hpp, independiente de cualquier representación de términos;term_to_binaryybinary_to_termdel runtime lo comparten (external term format). El modelo de términos del compilador de Clause contiene átomos, enteros, floats, listas propias e impropias, tuplas, maps y bitstrings; los pids, ports, referencias y funs se rechazan con un diagnóstico.- El texto
.abstres la sintaxis defile:consult/1: un término por forma, cada uno terminado en..--print-abstrimprime en stdout las formas de cada módulo después de las transformaciones en esa sintaxis, una forma por línea; un archivo de entrada.abstrse compila comoerlc File.abstr. Clause escribe texto canónico, no la disposición~pde OTP; lo que importa es que los términos sean iguales. Las formas son iguales aepp:parse_file(F, [{location, {1, 1}}])para cada construcción detests/fixtures/transforms/abstract/, incluidos los atributosfiledeepp(un-fileexplícito se anota[{generated, true}, {location, L}]) y uneofcontado desde el último-filedel archivo principal.
Importación de formas
Las formas devueltas, y las entradas .abstr (posicionales o sources del
proyecto), se convierten en tokens ubicados en sus anotaciones y pasan por el
parser de Clause, por lo que reciben las mismas comprobaciones sintácticas que
el código fuente; la precedencia de operadores decide dónde se añaden
paréntesis. Se aceptan {Line, Column}, un Line simple (mostrado como
file:line:) y listas de propiedades con location y file; generated se
ignora. La línea 0, que la transformación qlc de OTP asigna al código
generado, toma la ubicación del nodo importado antes de ella. Un atributo file
cambia el archivo de las formas siguientes; cuando ese archivo existe, el
código importado conserva sus líneas de depuración y los
comentarios del fuente. Las formas mal construidas son errores en su anotación
(unknown expression bogus, expected {Name, Arity}, invalid annotation) y
el módulo falla; las formas {error, _}/{warning, _} de OTP se informan con
su descriptor, que necesita OTP para formatearse. Importar un módulo exportado
y exportarlo de nuevo produce las mismas formas.
OTP del anfitrión
erlse busca en este orden:--erl PATH,erlenPATHy después, en Windows,%ProgramFiles%\Erlang OTP\bin\erl.exe. No encontrarlo cuando un módulo necesita una transformación es un error que nombra la transformación y el módulo; no hay ninguna alternativa que omita las transformaciones.- El anfitrión debe ejecutar OTP 29 (
erlang:system_info(otp_release)); otra versión es un error. - El cargador se ejecuta como
erl -noshell -noinput -pa Dir ... -eval ...(con los directorios de la ruta de código convertidos en absolutos) con un límite de tiempo de 600 s. Su stdout y su stderr (incluida la salidaio:formatde la transformación) se copian al stderr declaucuando termina. Un cargador que sale sin escribir una respuesta, agota el tiempo o no puede iniciarse es un error. - Una transformación ejecuta código arbitrario con los permisos del usuario,
exactamente como lo haría con
erlc.
Cargador
compiler/src/transforms/loader/clause_transform_loader.erl es propiedad del
proyecto y está incrustado en clau. Cada ejecución lo escribe junto con la
solicitud en un directorio temporal nuevo, clause-transform-* bajo
<build_dir>/tmp (directorio de compilación) o,
para entradas posicionales sin --build-dir, el directorio temporal del
sistema, que se elimina tras la ejecución; erl compila y carga el cargador en
memoria. La solicitud contiene las formas, las opciones, la lista de
transformaciones, el nombre de archivo del módulo y los fuentes de
transformaciones del proyecto que hay que compilar. Con un directorio de
compilación, la respuesta y la salida del cargador se guardan en la
caché de compilación y se reutilizan mientras la
solicitud y todo aquello de lo que depende no cambien; la lista de módulos
sticky del anfitrión se conserva de la misma manera. El cargador:
- comprueba la versión de OTP;
- compila en memoria los fuentes de transformaciones del proyecto solicitados y los carga;
- para cada transformación: la carga, elimina las columnas según lo pida
parse_transform_info/0, llama aparse_transform/2y formatea las advertencias y los errores como{File, Location, Text}conModule:format_error/1; - escribe
{ok, Forms, Warnings}o{error, Errors, Warnings}y se detiene.
Módulos de transformación
Un módulo de transformación se busca en este orden:
- Archivos
.beamprecompilados en los directorios de--transform-path DIR(repetible) y de la opción de proyectotransform_paths, que se colocan en la ruta de código del cargador antes que los de OTP. Esta es la única forma de usar una transformación que procede de una biblioteca de dependencia: primero se compila esa biblioteca con el OTP del anfitrión (por ejemploerlc -o deps/lib/ebin deps/lib/src/*.erl); Clause nunca compila dependencias. El.beamdebe proceder de la versión mayor de OTP del anfitrión. - Los fuentes del proyecto actual: un
Module.erldel lote o de sussource_search_paths, compilado en memoria por el cargador con los directorios de include y las definiciones del destino en cada ejecución, junto con los módulos del proyecto a los que llama por nombre (encontrados de la misma manera). La búsqueda omite los fuentes queignore_dirsoignore_modulesdejan fuera, como hace la búsqueda de módulos. Clause busca estos fuentes una vez por lote y los mantiene en memoria; el cargador sigue compilándolos para cada módulo sobre el que se ejecuta la transformación, así que conviene precompilar una transformación grande para ahorrar ese tiempo. Los módulos que el anfitrión carga desde sus directorios sticky (kernel,stdlib,compiler, comoms_transformoqlc_ptcuando los fuentes de OTP están en una ruta de búsqueda) no pueden recargarse, por lo que se usan los propios módulos del anfitrión: sus fuentes, y los módulos a los que solo ellos llaman, no se buscan ni se compilan. Clause pregunta alerldel anfitrión por estos módulos una vez por invocación. - Los propios módulos de OTP (
ms_transform,qlc) de la ruta de código del anfitrión. OTP incluyeerl_id_transsolo como fuente de ejemplo, no como módulo. Sus cabeceras se resuelven como cualquierinclude_lib(preprocesador): se asigna la aplicación, como en--app-dir "stdlib=C:/Program Files/Erlang OTP/lib/stdlib-8.1"parainclude_lib("stdlib/include/ms_transform.hrl"), o se nombra la transformación directamente con-compile({parse_transform, ms_transform}).
Compilar una transformación del proyecto puede fallar donde su compilación real
tiene éxito: cabeceras de dependencias, una transformación de la
transformación o macros que solo define otra herramienta de compilación.
Clause informa entonces de los mensajes del compilador sobre el fuente de la
transformación con la indicación de precompilarla en un .beam con el OTP del
anfitrión y pasar su directorio con --transform-path. Clause también compila
una transformación del proyecto como un módulo ordinario cuando forma parte del
lote; sus propias llamadas (erl_syntax, erl_parse, ...) pueden necesitar
entonces módulos de biblioteca que Clause no incluye, por lo que conviene
mantener las transformaciones fuera de los sources de un destino cuando solo
la transformación los necesita.
Diagnósticos
| Situación | Mensaje |
|---|---|
| Sin OTP en el anfitrión | File: parse transform 'M' needs Erlang/OTP 29 on the host; pass --erl |
--erl no nombra ningún ejecutable | File: erl not found: PATH |
| Versión de OTP incorrecta | parse transforms need Erlang/OTP 29 on the host, found N |
| Transformación no encontrada | File: undefined parse transform 'M' (texto de OTP) |
| La transformación lanzó una excepción | File: error in parse transform 'M': ... (texto de OTP) |
| Advertencia o error de la transformación | File:Line:Column: Text tal como se devuelve |
| El fuente de la transformación no compila | sus mensajes y después la indicación de precompilar |
| Una ruta de transformaciones no existe | File: parse transform path is not a directory: DIR |
Un .beam en una ruta de transformaciones no se carga | File: cannot load parse transform 'M' from PATH: Reason |
| El cargador falló, agotó el tiempo o no escribió respuesta | parse transform loader failed: ... |
| Formas devueltas no válidas | el diagnóstico del parser en la ubicación de la forma |
Opciones
| CLI | Proyecto ([targets.options]) | Significado |
|---|---|---|
--parse-transform M | parse_transforms = ["m"] | Aplica M a cada módulo, antes de sus propias transformaciones -compile (primero las de la CLI) |
--transform-path DIR | transform_paths = ["deps/*/ebin"] | Ruta de código para archivos .beam de transformaciones precompiladas; patrones como en source_search_paths |
--erl PATH o none | — | Ejecutable erl del anfitrión; none nunca ejecuta código del anfitrión |
--print-abstr | — | Imprime las formas después de las transformaciones en lugar de compilar |
Un proyecto que usa su propia transformación y una precompilada de una dependencia:
[[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