Parse transforms
Використання parse transforms
- Вимога: на хості встановлено Erlang/OTP 29, лише для компіляції модулів,
що використовують перетворення (transform) (OTP хоста).
--erl noneніколи не запускає код хоста: модулі, яким потрібне перетворення, не компілюються. - Виклик перетворення у вихідному коді через
-compile({parse_transform, Module}).(або всередині списку-compile([...])), або для кожного модуля команди через--parse-transform Module(можна повторювати; ці виконуються першими, якerlc +'{parse_transform,M}'). - Модулі перетворень шукаються як файли
.beamу шляхах перетворень, потім як вихідні файли проєкту, які OTP хоста компілює під час кожної збірки, потім серед модулів OTP (модулі перетворень). - Попередньо компілюйте перетворення з іншої бібліотеки (завжди) або
перетворення проєкту, яке компілятор хоста не може зібрати з каталогами
include і визначеннями цілі:
erlc -o deps/lib/ebin deps/lib/src/*.erlза допомогою OTP хоста, потім укажітьdeps/lib/ebinчерез--transform-pathабо вtransform_paths..beamмає походити з релізу OTP хоста;.beamтам має перевагу над вихідним файлом того самого модуля. - Що їх запускає: компіляція,
--print-ast,--print-sourceі--print-abstr(який показує перетворені форми);--parse-check,--preprocess-checkі--print-pp— ні.
clau --erl "C:/Program Files/Erlang OTP/bin/erl.exe" --transform-path ebin -o app app.erl
Clause застосовує {parse_transform, Module} так само, як erlc. Він
експортує розібраний модуль як форми OTP
abstract format, запускає
модуль перетворення на Erlang/OTP 29, встановленому на хості, імпортує
повернені форми як новий синтаксис модуля й компілює його
(compiler/src/transforms/). OTP потрібен лише під час компіляції модуля, що
використовує перетворення; згенерованим програмам він ніколи не потрібен.
Поведінка OTP
За зафіксованим lib/compiler/src/compile.erl (transform_module,
foldl_transform, maybe_strip_columns) і спостереженнями під OTP 29.1.1:
- Перетворення виконуються як перший стандартний прохід: після
eppі розбору, передerl_lint, перед тим якerl_internalдодаєmodule_info/0,1іbehaviour_info/1. - Список перетворень — це кожен
{parse_transform, M}з опцій компіляції (erlc +'{parse_transform,m}'), потім з атрибутів-compileу порядку форм. Перетворення, назване двічі, виконується двічі. Перед запуском першого ці опції видаляються з форм: атрибут-compile, що містить лише цю опцію, зникає, зі списку видаляється елемент. Mмає завантажуватися (code:ensure_loaded/1) і експортуватиparse_transform/2, інакше помилкаundefined parse transform 'M'(без розташування).M:parse_transform(Forms, Options)отримує форми й опції компіляції (не опції-compileз вихідного коду):{features, Enabled},report_warnings,report_errors,{cwd, Dir},{outdir, Dir},{i, Dir}для кожного каталогу include,{d, Name[, Value]}для кожного визначення, а також будь-який{parse_transform, M}, переданий як опція.- Вона повертає
Forms,{warning, Forms, Warnings}або{error, Errors, Warnings}, де кожен список має вигляд[{File, [{Location, Module, Descriptor}]}]і форматується черезModule:format_error(Descriptor). Виняток стає помилкоюerror in parse transform 'M': ...(compile:format_error/1від{parse_transform, M, {Class, Reason, Stack}}). Наступне перетворення отримує форми попереднього; перша помилка зупиняє ланцюжок. M:parse_transform_info/0, що повертає#{error_location => line}, або опція компіляції{error_location, line}прибирає стовпці з форм перед запускомMі з остаточних форм.- Форми мають анотації
{Line, Column}.eppдодає першим{attribute, {1,1}, file, {File, 1}}, те саме для кожного підключеного файлу на його початку, і{attribute, {L,1}, file, {File, L}}, коли файл, що підключає, продовжується з рядкаL, наступного після його-include. Визначення макросів та інші директиви не залишають форм. Список закінчується{eof, Location}— кінцем головного файлу. Помилки препроцесора й розбору лишаються в списку як форми{error, E}, іerl_lintповідомляє про них після перетворень. - Форми OTP 29, яких немає в
absform.md:{attribute, A, native_record, {Name, Fields}}; імена record{Module, Name}(кваліфіковані) і[](#_) у виразахrecord/record_field;{type, A, record, [{tuple, A, [Module, Name]} | Fields]}для кваліфікованих типів record; кваліфікатори comprehension{zip, A, Qualifiers};{mc, ...},m_generateі генератори_strict. erlc +to_abstrзаписує форми після перетворень (іerl_lint) уFile.abstr, по одному терму~p.на форму;erlc File.abstr(compile:file(F, [from_abstr])) компілює такий файл без препроцесора чи парсера.
Конвеєр Clause
- Препроцесувати й розібрати модуль. Помилки препроцесора чи розбору зупиняють роботу тут; перетворення не виконуються для модуля з помилками (відмінність від OTP, який передає помилки далі як форми).
- Зібрати перетворення в порядку OTP: опції
--parse-transformіparse_transformsпроєкту, потім атрибути-compile. Модуль без жодного пропускає решту й ніколи не запускає OTP. - Видалити ці опції з форм і експортувати їх: анотації
{Line, Column}так, як їх призначаєerl_parse, атрибутиfileвідeppіeof. Наперед визначених функцій у формах ще немає. - Один раз запустити завантажувач для модуля.
- Імпортувати повернені форми, перетворивши їх на токени, розташовані за
їхніми анотаціями, і розібравши парсером Clause; атрибут
fileзмінює файл наступних форм, як вerl_lint. - Додати наперед визначені функції, потім, як зазвичай, виконати lint, аналіз і компіляцію.
Дії, що запускають перетворення, перелічено в розділі використання parse transforms.
Обмін даними
clauі завантажувач обмінюються файлами External Term Format (term_to_binary, версія 131, без стиснення): точні float, атоми UTF-8, великі цілі числа й жодного розбору тексту в Erlang. Сам формат (теги, розміщення, перевірки меж) описано один раз вabi/include/clause/abi/external_term.hpp, незалежно від будь-якого представлення термів;term_to_binaryіbinary_to_termу runtime використовують його спільно (external term format). Модель термів компілятора Clause містить атоми, цілі числа, float, правильні й неправильні списки, кортежі, map і bitstring; pid, порти, посилання й fun відхиляються з діагностикою.- Текст
.abstrмає синтаксисfile:consult/1: один терм на форму, кожен закінчується..--print-abstrдрукує форми кожного модуля після перетворень у цьому синтаксисі в stdout, по одній формі на рядок; вхідний файл.abstrкомпілюється якerlc File.abstr. Clause записує канонічний текст, а не розкладку~pз OTP; важить рівність термів. Форми дорівнюютьepp:parse_file(F, [{location, {1, 1}}])для кожної конструкції зtests/fixtures/transforms/abstract/, включно з атрибутамиfileвідepp(явний-fileанотується як[{generated, true}, {location, L}]) іeof, відрахованим від останнього-fileголовного файлу.
Імпорт форм
Повернені форми та вхідні файли .abstr (позиційні або sources проєкту)
стають токенами, розташованими за їхніми анотаціями, і проходять через парсер
Clause, тож отримують ті самі синтаксичні перевірки, що й вихідний код;
пріоритет операторів визначає, де додаються дужки. Приймаються
{Line, Column}, просто Line (показується як file:line:) і списки
властивостей із location і file; generated ігнорується. Рядок 0,
який перетворення qlc з OTP надає згенерованому коду, отримує розташування
вузла, імпортованого перед ним. Атрибут file
змінює файл наступних форм; якщо цей файл існує, імпортований код зберігає
свої налагоджувальні рядки й коментарі з вихідним кодом. Некоректні форми є
помилками в місці їхньої анотації (unknown expression bogus,
expected {Name, Arity}, invalid annotation), і модуль не компілюється;
форми OTP {error, _}/{warning, _} повідомляються з їхнім дескриптором,
для форматування якого потрібен OTP. Імпорт експортованого модуля й повторний
експорт дають ті самі форми.
OTP хоста
erlшукається в такому порядку:--erl PATH,erlзPATH, потім у Windows%ProgramFiles%\Erlang OTP\bin\erl.exe. Якщо його не знайдено, коли модулю потрібне перетворення, це помилка з назвою перетворення й модуля; запасного варіанта, що пропускає перетворення, немає.- Хост має працювати на OTP 29 (
erlang:system_info(otp_release)); інший реліз — помилка. - Завантажувач запускається як
erl -noshell -noinput -pa Dir ... -eval ...(каталоги шляху коду перетворено на абсолютні) з обмеженням часу 600 с. Його stdout і stderr (включно з виводомio:formatперетворення) копіюються в stderrclauпісля його завершення. Завантажувач, що завершується без запису відповіді, перевищує час або не може запуститися, — це помилка. - Перетворення виконує довільний код із правами користувача, точно так само,
як під
erlc.
Завантажувач
compiler/src/transforms/loader/clause_transform_loader.erl належить проєкту
і вбудований у clau. Кожен запуск записує його й запит у новий тимчасовий
каталог, clause-transform-* у <build_dir>/tmp
(каталог збірки) або, для позиційних вхідних
файлів без --build-dir, у системний тимчасовий каталог, який видаляється
після запуску; erl компілює й завантажує завантажувач у пам'яті. Запит
містить форми, опції, список перетворень, ім'я файлу модуля і вихідні файли
перетворень проєкту, які треба зібрати. З каталогом збірки відповідь і вивід
завантажувача зберігаються в кеші збірки й
використовуються повторно, поки запит і все, від чого він залежить, не
змінюються; список sticky-модулів хоста зберігається так само. Завантажувач:
- перевіряє реліз OTP;
- компілює запитані вихідні файли перетворень проєкту в пам'яті й завантажує їх;
- для кожного перетворення: завантажує його, прибирає стовпці, як просить
parse_transform_info/0, викликаєparse_transform/2і форматує попередження й помилки в{File, Location, Text}черезModule:format_error/1; - записує
{ok, Forms, Warnings}або{error, Errors, Warnings}і зупиняється.
Модулі перетворень
Модуль перетворення шукається в такому порядку:
- Попередньо скомпільовані файли
.beamу каталогах--transform-path DIR(можна повторювати) і опції проєктуtransform_paths, які потрапляють у шлях коду завантажувача перед каталогами OTP. Це єдиний спосіб використати перетворення з бібліотеки-залежності: спершу зберіть цю бібліотеку за допомогою OTP хоста (наприклад,erlc -o deps/lib/ebin deps/lib/src/*.erl); Clause ніколи не збирає залежності..beamмає походити з того самого мажорного релізу OTP, що й хост. - Вихідні файли поточного проєкту:
Module.erlу пакеті або в йогоsource_search_paths, який завантажувач компілює в пам'яті з каталогами include і визначеннями цілі під час кожного запуску, разом із модулями проєкту, які він викликає за іменем (знайденими так само). Пошук пропускає вихідні файли, які виключаютьignore_dirsабоignore_modules, як це робить пошук модулів. Clause шукає ці вихідні файли один раз на пакет і тримає їх у пам'яті; завантажувач однаково компілює їх для кожного модуля, до якого застосовується перетворення, тож велике перетворення варто попередньо скомпілювати, щоб заощадити цей час. Модулі, які хост завантажує зі своїх sticky-каталогів (kernel,stdlib,compiler, як-отms_transformчиqlc_pt, коли вихідні файли OTP є в шляху пошуку), не можна перезавантажити, тому використовуються власні модулі хоста: їхні вихідні файли й модулі, які викликають лише вони, не шукаються й не компілюються. Clause запитує вerlхоста ці модулі один раз за виклик. - Власні модулі OTP (
ms_transform,qlc) зі шляху коду хоста. OTP постачаєerl_id_transлише як приклад вихідного коду, а не як модуль. Їхні заголовки розв'язуються так само, як коженinclude_lib(препроцесор): зіставте застосунок, як у--app-dir "stdlib=C:/Program Files/Erlang OTP/lib/stdlib-8.1"дляinclude_lib("stdlib/include/ms_transform.hrl"), або назвіть перетворення напряму через-compile({parse_transform, ms_transform}).
Компіляція перетворення проєкту може завершитися невдачею там, де його
справжня збірка вдається: заголовки залежностей, перетворення самого
перетворення або макроси, які визначає лише інший інструмент збірки. Тоді
Clause виводить повідомлення компілятора щодо вихідного файлу перетворення
з підказкою попередньо скомпілювати його в .beam за допомогою OTP хоста й
передати його каталог через --transform-path. Перетворення проєкту також
компілюється Clause як звичайний модуль, коли воно входить до пакета; тоді
його власним викликам (erl_syntax, erl_parse, ...) можуть знадобитися
бібліотечні модулі, яких Clause не постачає, тож не додавайте перетворення до
sources цілі, якщо вони потрібні лише перетворенню.
Діагностика
| Ситуація | Повідомлення |
|---|---|
| Немає OTP на хості | File: parse transform 'M' needs Erlang/OTP 29 on the host; pass --erl |
--erl не вказує на виконуваний файл | File: erl not found: PATH |
| Неправильний реліз OTP | parse transforms need Erlang/OTP 29 on the host, found N |
| Перетворення не знайдено | File: undefined parse transform 'M' (текст OTP) |
| Перетворення згенерувало виняток | File: error in parse transform 'M': ... (текст OTP) |
| Попередження чи помилка перетворення | File:Line:Column: Text як повернуто |
| Вихідний файл перетворення не компілюється | його повідомлення, потім підказка про попередню компіляцію |
| Шлях перетворень не існує | File: parse transform path is not a directory: DIR |
.beam у шляху перетворень не завантажується | File: cannot load parse transform 'M' from PATH: Reason |
| Завантажувач збоїв, перевищив час або не записав відповіді | parse transform loader failed: ... |
| Некоректні повернені форми | діагностика парсера в місці форми |
Опції
| CLI | Проєкт ([targets.options]) | Значення |
|---|---|---|
--parse-transform M | parse_transforms = ["m"] | Застосувати M до кожного модуля перед його власними перетвореннями з -compile (спершу ті, що з CLI) |
--transform-path DIR | transform_paths = ["deps/*/ebin"] | Шлях коду для попередньо скомпільованих файлів .beam перетворень; шаблони як у source_search_paths |
--erl PATH або none | — | Виконуваний файл erl хоста; none ніколи не запускає код хоста |
--print-abstr | — | Надрукувати форми після перетворень замість компіляції |
Проєкт, що використовує власне перетворення і попередньо скомпільоване з залежності:
[[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