Parse transforms
Utilizar parse transforms
- Requisito: Erlang/OTP 29 instalado no anfitrião, apenas para compilar
módulos que usam um transform (OTP do anfitrião).
--erl nonenunca executa código do anfitrião: os módulos que precisam de um transform falham. - Invocar um transform no código-fonte com
-compile({parse_transform, Module}).(ou dentro de uma lista-compile([...])), ou para todos os módulos do comando com--parse-transform Module(repetível; estes correm primeiro, comoerlc +'{parse_transform,M}'). - Os módulos de transform são encontrados como ficheiros
.beamnos caminhos de transforms, depois como códigos-fonte do projeto compilados pelo OTP do anfitrião em cada compilação, depois entre os módulos do OTP (módulos de transform). - Pré-compilar um transform que vem de outra biblioteca (sempre), ou um
transform do projeto que o compilador do anfitrião não consegue compilar com
os diretórios de include e as definições do alvo:
erlc -o deps/lib/ebin deps/lib/src/*.erlcom o OTP do anfitrião, e depois indicardeps/lib/ebincom--transform-pathou emtransform_paths. O.beamtem de vir da release do OTP do anfitrião; um.beamaí prevalece sobre um código-fonte do mesmo módulo. - O que o executa: a compilação,
--print-ast,--print-sourcee--print-abstr(que mostra as formas transformadas);--parse-check,--preprocess-checke--print-ppnão o executam.
clau --erl "C:/Program Files/Erlang OTP/bin/erl.exe" --transform-path ebin -o app app.erl
O Clause aplica {parse_transform, Module} tal como o erlc. Exporta o módulo
analisado como formas do
formato abstrato do OTP,
executa o módulo de transform num Erlang/OTP 29 instalado no anfitrião, importa
as formas devolvidas como a nova sintaxe do módulo e compila-a
(compiler/src/transforms/). O OTP só é necessário enquanto se compila um
módulo que usa um transform; os programas gerados nunca precisam dele.
Comportamento do OTP
A partir de lib/compiler/src/compile.erl fixado (transform_module,
foldl_transform, maybe_strip_columns) e observado com o OTP 29.1.1:
- Os transforms correm como a primeira passagem padrão: depois do
eppe da análise sintática, antes doerl_lint, antes de oerl_internalacrescentarmodule_info/0,1ebehaviour_info/1. - A lista de transforms é cada
{parse_transform, M}das opções de compilação (erlc +'{parse_transform,m}') e depois dos atributos-compile, pela ordem das formas. Um transform indicado duas vezes corre duas vezes. Antes de o primeiro correr, essas opções são removidas das formas: um atributo-compileque contenha apenas a opção desaparece, uma lista perde o elemento. Mtem de carregar (code:ensure_loaded/1) e exportarparse_transform/2; caso contrário, dá o erroundefined parse transform 'M'(sem localização).M:parse_transform(Forms, Options)recebe as formas e as opções de compilação (não as opções-compiledo código-fonte):{features, Enabled},report_warnings,report_errors,{cwd, Dir},{outdir, Dir},{i, Dir}por diretório de include,{d, Name[, Value]}por definição, mais qualquer{parse_transform, M}dado como opção.- Devolve
Forms,{warning, Forms, Warnings}ou{error, Errors, Warnings}, em que cada lista é[{File, [{Location, Module, Descriptor}]}]formatada porModule:format_error(Descriptor). Uma exceção é o erroerror in parse transform 'M': ...(compile:format_error/1de{parse_transform, M, {Class, Reason, Stack}}). O transform seguinte recebe as formas do anterior; o primeiro erro interrompe a cadeia. M:parse_transform_info/0a devolver#{error_location => line}, ou a opção de compilação{error_location, line}, retira as colunas das formas antes deMcorrer e das formas finais.- As formas levam anotações
{Line, Column}. Oeppacrescenta primeiro{attribute, {1,1}, file, {File, 1}}, o mesmo para cada ficheiro incluído quando começa, e{attribute, {L,1}, file, {File, L}}quando o ficheiro que inclui continua na linhaL, a linha a seguir ao seu-include. As definições de macros e as outras diretivas não deixam nenhuma forma. A lista termina com{eof, Location}, o fim do ficheiro principal. Os erros do pré-processador e de análise sintática ficam na lista como formas{error, E}e oerl_lintcomunica-os depois dos transforms. - Formas do OTP 29 que o
absform.mdnão lista:{attribute, A, native_record, {Name, Fields}}; nomes de records{Module, Name}(qualificados) e[](#_) em expressõesrecord/record_field;{type, A, record, [{tuple, A, [Module, Name]} | Fields]}para tipos de records qualificados;{zip, A, Qualifiers}como qualificadores de comprehensions;{mc, ...},m_generatee os geradores_strict. erlc +to_abstrescreve as formas depois dos transforms (e doerl_lint) emFile.abstr, um termo~p.por forma;erlc File.abstr(compile:file(F, [from_abstr])) compila um ficheiro desses sem pré-processador nem parser.
Pipeline do Clause
- Pré-processar e analisar o módulo. Os erros do pré-processador ou da análise sintática param aqui; os transforms não correm num módulo com erros (difere do OTP, que passa os erros adiante como formas).
- Recolher os transforms pela ordem do OTP: as opções
--parse-transforme osparse_transformsdo projeto, depois os atributos-compile. Um módulo sem nenhum salta o resto e nunca inicia o OTP. - Remover essas opções das formas e exportá-las: anotações
{Line, Column}tal como oerl_parseas atribui, os atributosfiledoeppeeof. As funções predefinidas ainda não fazem parte das formas. - Executar o carregador uma vez para o módulo.
- Importar as formas devolvidas transformando-as em tokens localizados nas
suas anotações e analisando-as com o parser do Clause; um atributo
filemuda o ficheiro das formas seguintes, como noerl_lint. - Acrescentar as funções predefinidas e depois fazer o lint, analisar e compilar como habitualmente.
As ações que executam transforms estão listadas em utilizar parse transforms.
Intercâmbio
- O
claue o carregador trocam ficheiros no External Term Format (term_to_binary, versão 131, não comprimido): floats exatos, átomos UTF-8, inteiros grandes e nenhuma análise de texto em Erlang. O próprio formato (etiquetas, disposições, verificações de limites) existe uma única vez emabi/include/clause/abi/external_term.hpp, independente de qualquer representação de termos; osterm_to_binaryebinary_to_termdo runtime partilham-no (external term format). O modelo de termos do compilador do Clause contém átomos, inteiros, floats, listas próprias e impróprias, tuplos, maps e bitstrings; os pids, ports, referências e funs são rejeitados com um diagnóstico. - O texto
.abstrusa a sintaxe defile:consult/1: um termo por forma, cada um terminado em..--print-abstrimprime no stdout as formas de cada módulo depois dos transforms nessa sintaxe, uma forma por linha; um ficheiro de entrada.abstrcompila comoerlc File.abstr. O Clause escreve texto canónico, não a disposição~pdo OTP; o que importa são termos iguais. As formas são iguais aepp:parse_file(F, [{location, {1, 1}}])para todas as construções detests/fixtures/transforms/abstract/, incluindo os atributosfiledoepp(um-fileexplícito é anotado[{generated, true}, {location, L}]) e umeofcontado a partir do último-filedo ficheiro principal.
Importar formas
As formas devolvidas, e as entradas .abstr (posicionais ou nos sources do
projeto), tornam-se tokens localizados nas suas anotações e passam pelo parser
do Clause, pelo que recebem as mesmas verificações de sintaxe que o
código-fonte; a precedência dos operadores decide onde são acrescentados
parênteses. São aceites {Line, Column}, um Line simples (mostrado como
file:line:) e listas de propriedades com location e file; generated é
ignorado. A linha 0, que o transform qlc do OTP atribui ao código gerado,
assume a localização do nó importado antes dela. Um atributo file muda o
ficheiro das formas seguintes; quando esse
ficheiro existe, o código importado mantém as suas linhas de depuração e os
comentários de código-fonte. As formas mal formadas são erros na sua anotação
(unknown expression bogus, expected {Name, Arity}, invalid annotation) e
o módulo falha; as formas {error, _}/{warning, _} do OTP são comunicadas
com o seu descritor, cuja formatação precisa do OTP. Importar um módulo
exportado e exportá-lo de novo dá as mesmas formas.
OTP do anfitrião
- O
erlé procurado por esta ordem:--erl PATH,erlnoPATHe depois, em Windows,%ProgramFiles%\Erlang OTP\bin\erl.exe. Não encontrar nenhum quando um módulo precisa de um transform é um erro que indica o transform e o módulo; não há alternativa que salte os transforms. - O anfitrião tem de executar o OTP 29 (
erlang:system_info(otp_release)); outra release é um erro. - O carregador corre como
erl -noshell -noinput -pa Dir ... -eval ...(diretórios do caminho de código tornados absolutos) com um limite de tempo de 600 s. O seu stdout e stderr (incluindo a saídaio:formatdo transform) são copiados para o stderr doclaudepois de terminar. Um carregador que sai sem escrever uma resposta, esgota o tempo ou não consegue arrancar é um erro. - Um transform executa código arbitrário com os direitos do utilizador,
exatamente como faria com o
erlc.
Carregador
compiler/src/transforms/loader/clause_transform_loader.erl pertence ao
projeto e está embutido no clau. Cada execução escreve-o, juntamente com o
pedido, num diretório temporário novo, clause-transform-* abaixo de
<build_dir>/tmp (diretório de compilação) ou,
para entradas posicionais sem --build-dir, no diretório temporário do
sistema, removido após a execução; o erl compila e carrega o carregador em
memória. O pedido contém as formas, as opções, a lista de transforms, o nome do
ficheiro do módulo e os códigos-fonte de transforms do projeto a compilar. Com
um diretório de compilação, a resposta e a saída do carregador são guardadas na
cache de compilação e reutilizadas enquanto o
pedido e tudo aquilo de que depende se mantiverem inalterados; a lista de
módulos sticky do anfitrião é guardada da mesma forma. O carregador:
- verifica a release do OTP;
- compila em memória os códigos-fonte de transforms do projeto pedidos e carrega-os;
- para cada transform: carrega-o, retira as colunas conforme
parse_transform_info/0pedir, chamaparse_transform/2e formata os avisos e os erros como{File, Location, Text}comModule:format_error/1; - escreve
{ok, Forms, Warnings}ou{error, Errors, Warnings}e termina.
Módulos de transform
Um módulo de transform é procurado por esta ordem:
- Ficheiros
.beampré-compilados nos diretórios de--transform-path DIR(repetível) e da opção de projetotransform_paths, que entram no caminho de código do carregador antes dos do OTP. É a única forma de usar um transform que vem de uma biblioteca de dependência: compile primeiro essa biblioteca com o OTP do anfitrião (por exemploerlc -o deps/lib/ebin deps/lib/src/*.erl); o Clause nunca compila dependências. O.beamtem de vir da release principal do OTP do anfitrião. - Os códigos-fonte do projeto atual: um
Module.erlno lote ou nos seussource_search_paths, compilado pelo carregador em memória com os diretórios de include e as definições do alvo em cada execução, juntamente com os módulos do projeto que chama pelo nome (encontrados da mesma forma). A pesquisa ignora os códigos-fonte queignore_dirsouignore_modulesdeixam de fora, tal como faz a pesquisa de módulos. O Clause procura estes códigos-fonte uma vez por lote e mantém-nos em memória; o carregador continua a compilá-los para cada módulo em que o transform corre, pelo que convém pré-compilar um transform grande para poupar esse tempo. Os módulos que o anfitrião carrega dos seus diretórios sticky (kernel,stdlib,compiler, comoms_transformouqlc_ptquando os códigos-fonte do OTP estão num caminho de pesquisa) não podem ser recarregados, pelo que são usados os próprios módulos do anfitrião: os seus códigos-fonte, e os módulos que só eles chamam, não são procurados nem compilados. O Clause pergunta aoerldo anfitrião por estes módulos uma vez por invocação. - Os próprios módulos do OTP (
ms_transform,qlc) a partir do caminho de código do anfitrião. O OTP incluierl_id_transapenas como código-fonte de exemplo, não como módulo. Os seus cabeçalhos resolvem-se como qualquerinclude_lib(pré-processador): mapeie a aplicação, como em--app-dir "stdlib=C:/Program Files/Erlang OTP/lib/stdlib-8.1"parainclude_lib("stdlib/include/ms_transform.hrl"), ou indique o transform diretamente com-compile({parse_transform, ms_transform}).
Compilar um transform do projeto pode falhar onde a sua compilação real tem
sucesso: cabeçalhos de dependências, um transform do transform, ou macros que
só outra ferramenta de compilação define. O Clause comunica então as mensagens
do compilador em relação ao código-fonte do transform, com a sugestão de o
pré-compilar num .beam com o OTP do anfitrião e de passar o seu diretório com
--transform-path. Um transform do projeto também é compilado pelo Clause como
um módulo comum quando faz parte do lote; as suas próprias chamadas
(erl_syntax, erl_parse, ...) podem então precisar de módulos de biblioteca
que o Clause não inclui, pelo que convém manter os transforms fora dos
sources de um alvo quando só o transform precisa deles.
Diagnósticos
| Situação | Mensagem |
|---|---|
| Sem OTP no anfitrião | File: parse transform 'M' needs Erlang/OTP 29 on the host; pass --erl |
--erl não indica nenhum executável | File: erl not found: PATH |
| Release do OTP errada | parse transforms need Erlang/OTP 29 on the host, found N |
| Transform não encontrado | File: undefined parse transform 'M' (texto do OTP) |
| O transform lançou uma exceção | File: error in parse transform 'M': ... (texto do OTP) |
| Aviso ou erro do transform | File:Line:Column: Text tal como devolvido |
| O código-fonte do transform não compila | as suas mensagens e depois a sugestão de pré-compilação |
| Um caminho de transforms não existe | File: parse transform path is not a directory: DIR |
Um .beam num caminho de transforms não carrega | File: cannot load parse transform 'M' from PATH: Reason |
| O carregador falhou, esgotou o tempo ou não escreveu resposta | parse transform loader failed: ... |
| Formas devolvidas inválidas | o diagnóstico do parser na localização da forma |
Opções
| CLI | Projeto ([targets.options]) | Significado |
|---|---|---|
--parse-transform M | parse_transforms = ["m"] | Aplica M a todos os módulos, antes dos seus próprios transforms -compile (os da CLI primeiro) |
--transform-path DIR | transform_paths = ["deps/*/ebin"] | Caminho de código para ficheiros .beam de transforms pré-compilados; padrões como em source_search_paths |
--erl PATH ou none | — | Executável erl do anfitrião; none nunca executa código do anfitrião |
--print-abstr | — | Imprime as formas depois dos transforms em vez de compilar |
Um projeto que usa o seu próprio transform e um pré-compilado de uma dependência:
[[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