_Эта статья является переводом поста Chris Hodapp [Embedding Haskell: Compilers, and compiling compilers](https://haskellembedded.github.io/posts/2015-10-09-compiler-compilers.html) В своём посте автор рассматривает различные подходы к использованию Haskell для написания кода для встраиваемых систем. Предоставим слово автору._
В моем последнем [посте](https://haskellembedded.github.io/posts/2015-06-09-atom-cincyfp-slides.html) упоминалось, что некоторые вещи требуют лучшего объяснения, потому что я всегда пытаюсь объяснить и уточнить.
Этот блог посвящен использованию Haskell со встраиваемыми системами. Что это хотя бы значит? Мы видим пару широких категорий (которые отражают слайды на последней странице, а также наша страница [ссылок](https://haskellembedded.github.io/pages/links.html)):
— Полная компиляция: компиляция кода на Haskell для встраиваемого назначения.
— Ограниченная компиляция: компиляция некоторого ограниченного подмножества кода на Haskell для встраиваемого назначения.
— Хостинг EDSL и компилятора: хостинг в Haskell, EDSL и компилятор для встраиваемого назначения.
Насколько мне известно, эти категории придумал я. Если кому-то известна более устоявшаяся классификация, более подходящие названия или примеры того, кто написал об этом первым, сообщите, пожалуйста.
Это может выглядеть как односторонняя, произвольная группировка; это вроде так. Общность заключается в том, что во всех случаях Haskell используется для выражения чего-либо (программы, схемы, спецификации, называйте это как хотите) для встраиваемого назначения. Подробнее об этом далее.
Я исключаю такие вещи, как Cryptol и Idris, потому что, будучи реализованными на Haskell и пригодными для встраиваемых платформ, они сами по себе являются другими языками. Я могу произвольно отбросить это различие в будущем, если захочу…
## Полная компиляция
Это то, что обычно приходит на ум, когда люди слышат об использовании Haskell со встраиваемыми системами — компиляция кода на Haskell для запуска непосредственно во встраиваемой системе, в сочетании с обычной средой выполнения (плюс все, что требуется для начальной загрузки и поддержки). Раздел [Compiling to Embedded Targets](https://haskellembedded.github.io/pages/links.html#compiling-to-embedded-targets) на странице ссылок будет особенно интересен в этом отношении.
Тем не менее, это на самом деле кажется довольно редким. Природа языка Haskell ставит некоторые сложные задачи. В частности, необходимо обеспечить соответствие среды выполнения Haskell целевой системе и заставить сборщик мусора и ленивые вычисления вести себя предсказуемым и вменяемым образом.
[Ajhc](https://github.com/ajhc/ajhc), компилятор, производный от [JHC](http://repetae.net/computer/jhc/), от Kiwamu Okabe из METASEPI, является единственным таким примером, который я обнаружил — он может компилироваться и выполняться на ARM Cortex-M3 / M4. Kiwamu много писал о своем опыте работы с Haskell по этим следам. Его последующее переключение на язык ATS может быть подсказкой.
[HaLVM](https://github.com/galoisinc/halvm) от Galois, возможно, вписывается в эту категорию.
## Ограниченная компиляция
В этом случае используется существующий компилятор для определенных этапов (таких как синтаксический анализ и проверка типов), и пользовательский бэкэнд для фактического создания кода, часто с большим количеством статического анализа. Здесь может быть адаптация или же запрет определенных конструкций (например, с плавающей точкой, рекурсивные функции, рекурсивные типы данных: [Неподдерживаемые CλaSH возможности Haskell](http://hackage.haskell.org/package/clash-prelude-0.10/docs/CLaSH-Tutorial.html#unsupported)).
GHC обеспечивает это, позволяя разработчикам вызывать функциональность GHC из Haskell в качестве [библиотеки](https://wiki.haskell.org/GHC/As_a_library).
В разделе [Compiling for FPGA/ASIC](https://haskellembedded.github.io/pages/links.html#compiling-for-fpgaasic) есть несколько примеров такого.
## Хостинг EDSL и компилятора
Разделы [Code Generation EDSLs](https://haskellembedded.github.io/pages/links.html#code-generation-edsls) и [Circuit Design EDSLs](https://haskellembedded.github.io/pages/links.html#circuit-design-edsls) на странице ссылок содержат множество примеров этого. _Атом_, тема нескольких моих предыдущих постов, находится в этой же категории.
Именно эту категорию мне чаще всего приходится объяснять. Обычно здесь используется EDSL (Embedded Domain-Specific Language, встроенный предметно-ориентированный язык) внутри Haskell, чтобы направить процесс генерации кода в представление более низкого уровня. Иначе это называется компиляция.
Подчеркнем: код, который выполняется на целевой системе, полностью отделен от среды выполнения Haskell. Компилятор Haskell здесь ничего не компилирует для целевой среды — он компилирует другой компилятор и ввод для этого компилятора. Эти входные данные являются спецификациями того, что будет работать на целевой системе.
Здесь есть ограничения одного вида:
— В основном все понятия времени выполнения во встраиваемой целевой среде должны поддерживаться отдельно. (_Ivory_ работает над этим до сих пор, например с классом типов [Num](https://hackage.haskell.org/package/base/docs/Prelude.html#t:Num), в некоторых удивительных отношениях. Подробнее об этом будет рассказано в следующем посте!)
— Это добавляет путаницу и усложнение другого этапа (возможно, нескольких этапов) к процессу связывания кода / спецификаций со встроенной средой. Вот почему я использую [Shake](https://shakebuild.com/).
Этот подход также даёт преимущество другого рода:
— Любая среда Haskell, совместимая с рассматриваемыми библиотеками, должна давать те же результаты (насколько это касается встраиваемой среды). Её среда времени исполнения не имеет значения, не имеет значения и окружение, которое знает об архитектуре встроенной системы.
— Такое разделение этапов также добавляет прекрасную возможность для статического анализа и оптимизации. Например, Copilot использует это для добавления интерпретатора / симулятора, SBV использует его для доказательства или опровержения заданных свойств кода, а Atom использует его для проверки некоторых временных ограничений.
В последнем посте я говорил о том, что в этой категории Haskell берет на себя роль метапрограммирования или языка шаблонов. Хотя это может быть правдой, я вроде проигнорировал, что это менее актуально, потому что это будет также и во всех других категориях.
## Общность
Объединение этих категорий может показаться натяжкой, особенно если учесть, что последняя категория включает дополнительные этапы и сдвиг, заключающийся в том, как человек думает о программном обеспечении.
Обдумайте следующее:
— «Нормальная» программа на Haskell взаимодействует через то, что упорядочено в известной монаде ввода-вывода (в частности, значение, называемое main).
— Спецификация Atom взаимодействует через то, что упорядочено в монаде [Atom](https://hackage.haskell.org/package/atom-1.0.13/docs/Language-Atom.html#t:Atom) (в частности, любые значения, передаваемые компилятору [Atom](https://hackage.haskell.org/package/atom-1.0.13/docs/Language-Atom.html#g:2)).
— Программа на Ivory взаимодействует через то, что упорядочено в монадах [Ivory eff](https://hackage.haskell.org/package/ivory-0.1.0.0/docs/Ivory-Language.html#t:Ivory)
и [Module](https://hackage.haskell.org/package/ivory-0.1.0.0/docs/Ivory-Language.html#t:Module) (в частности, какие бы значения ни передавались компилятору [Ivory](https://hackage.haskell.org/package/ivory-backend-c-0.1.0.1/docs/Ivory-Compile-C.html)).
— Описание [CλaSH](https://clash-lang.org/) взаимодействует через аппликативный [Signal](https://hackage.haskell.org/package/clash-prelude-0.10/docs/CLaSH-Signal.html#t:Signal) (в частности, значение, называемое [topEntity](https://hackage.haskell.org/package/clash-prelude-0.10/docs/CLaSH-Annotations-TopEntity.html)).
Тенденция ясна? (Нет, это не монады. Сигнал только аппликативен, и я подозреваю, что Lava ведет себя аналогично.)
Этот список охватывает наши три категории. В каждой из них создается программа (в широком смысле), просто создавая значение в Haskell. Помимо этого, единственные реальные различия это:
— тип этого значения,
— какая система его обрабатывает (компилятор и среда исполнения Haskell, какой-то другой компилятор и, возможно, среда исполнения или их комбинация),
— и возможный вывод (собственный двоичный файл, битовый код LLVM, код на C, код VHDL, код на ассемблере, вход в средство проверки модели и т.д.).
Игнорирование расплывчатой природы термина «декларативный» довольно напрямую связано с декларативным характером программ на Haskell.
С этой точки зрения, Haskell все еще компилируется для запуска в какой-то встраиваемой среде. При этом компиляция может продолжаться вне системного компилятора Haskell, и запуск может не использовать его среду исполнения.
© Перевод, Симоненко Е.А., 2020