Эта статья является переводом поста Chris Hodapp Embedding Haskell: Compilers, and compiling compilers В своём посте автор рассматривает различные подходы к использованию Haskell для написания кода для встраиваемых систем. Предоставим слово автору.


В моем последнем посте упоминалось, что некоторые вещи требуют лучшего объяснения, потому что я всегда пытаюсь объяснить и уточнить.


Этот блог посвящен использованию Haskell со встраиваемыми системами. Что это хотя бы значит? Мы видим пару широких категорий (которые отражают слайды на последней странице, а также наша страница ссылок):


  • Полная компиляция: компиляция кода на Haskell для встраиваемого назначения.
  • Ограниченная компиляция: компиляция некоторого ограниченного подмножества кода на Haskell для встраиваемого назначения.
  • Хостинг EDSL и компилятора: хостинг в Haskell, EDSL и компилятор для встраиваемого назначения.

Насколько мне известно, эти категории придумал я. Если кому-то известна более устоявшаяся классификация, более подходящие названия или примеры того, кто написал об этом первым, сообщите, пожалуйста.


Это может выглядеть как односторонняя, произвольная группировка; это вроде так. Общность заключается в том, что во всех случаях Haskell используется для выражения чего-либо (программы, схемы, спецификации, называйте это как хотите) для встраиваемого назначения. Подробнее об этом далее.


Я исключаю такие вещи, как Cryptol и Idris, потому что, будучи реализованными на Haskell и пригодными для встраиваемых платформ, они сами по себе являются другими языками. Я могу произвольно отбросить это различие в будущем, если захочу...


Полная компиляция


Это то, что обычно приходит на ум, когда люди слышат об использовании Haskell со встраиваемыми системами — компиляция кода на Haskell для запуска непосредственно во встраиваемой системе, в сочетании с обычной средой выполнения (плюс все, что требуется для начальной загрузки и поддержки). Раздел Compiling to Embedded Targets на странице ссылок будет особенно интересен в этом отношении.


Тем не менее, это на самом деле кажется довольно редким. Природа языка Haskell ставит некоторые сложные задачи. В частности, необходимо обеспечить соответствие среды выполнения Haskell целевой системе и заставить сборщик мусора и ленивые вычисления вести себя предсказуемым и вменяемым образом.


Ajhc, компилятор, производный от JHC, от Kiwamu Okabe из METASEPI, является единственным таким примером, который я обнаружил — он может компилироваться и выполняться на ARM Cortex-M3 / M4. Kiwamu много писал о своем опыте работы с Haskell по этим следам. Его последующее переключение на язык ATS может быть подсказкой.


HaLVM от Galois, возможно, вписывается в эту категорию.


Ограниченная компиляция


В этом случае используется существующий компилятор для определенных этапов (таких как синтаксический анализ и проверка типов), и пользовательский бэкэнд для фактического создания кода, часто с большим количеством статического анализа. Здесь может быть адаптация или же запрет определенных конструкций (например, с плавающей точкой, рекурсивные функции, рекурсивные типы данных: Неподдерживаемые CλaSH возможности Haskell).


GHC обеспечивает это, позволяя разработчикам вызывать функциональность GHC из Haskell в качестве библиотеки.


В разделе Compiling for FPGA/ASIC есть несколько примеров такого.


Хостинг EDSL и компилятора


Разделы Code Generation EDSLs и Circuit Design EDSLs на странице ссылок содержат множество примеров этого. Атом, тема нескольких моих предыдущих постов, находится в этой же категории.


Именно эту категорию мне чаще всего приходится объяснять. Обычно здесь используется EDSL (Embedded Domain-Specific Language, встроенный предметно-ориентированный язык) внутри Haskell, чтобы направить процесс генерации кода в представление более низкого уровня. Иначе это называется компиляция.


Подчеркнем: код, который выполняется на целевой системе, полностью отделен от среды выполнения Haskell. Компилятор Haskell здесь ничего не компилирует для целевой среды — он компилирует другой компилятор и ввод для этого компилятора. Эти входные данные являются спецификациями того, что будет работать на целевой системе.


Здесь есть ограничения одного вида:


  • В основном все понятия времени выполнения во встраиваемой целевой среде должны поддерживаться отдельно. (Ivory работает над этим до сих пор, например с классом типов Num, в некоторых удивительных отношениях. Подробнее об этом будет рассказано в следующем посте!)
  • Это добавляет путаницу и усложнение другого этапа (возможно, нескольких этапов) к процессу связывания кода / спецификаций со встроенной средой. Вот почему я использую Shake.

Этот подход также даёт преимущество другого рода:


  • Любая среда Haskell, совместимая с рассматриваемыми библиотеками, должна давать те же результаты (насколько это касается встраиваемой среды). Её среда времени исполнения не имеет значения, не имеет значения и окружение, которое знает об архитектуре встроенной системы.
  • Такое разделение этапов также добавляет прекрасную возможность для статического анализа и оптимизации. Например, Copilot использует это для добавления интерпретатора / симулятора, SBV использует его для доказательства или опровержения заданных свойств кода, а Atom использует его для проверки некоторых временных ограничений.

В последнем посте я говорил о том, что в этой категории Haskell берет на себя роль метапрограммирования или языка шаблонов. Хотя это может быть правдой, я вроде проигнорировал, что это менее актуально, потому что это будет также и во всех других категориях.


Общность


Объединение этих категорий может показаться натяжкой, особенно если учесть, что последняя категория включает дополнительные этапы и сдвиг, заключающийся в том, как человек думает о программном обеспечении.


Обдумайте следующее:


  • «Нормальная» программа на Haskell взаимодействует через то, что упорядочено в известной монаде ввода-вывода (в частности, значение, называемое main).
  • Спецификация Atom взаимодействует через то, что упорядочено в монаде Atom (в частности, любые значения, передаваемые компилятору Atom).
  • Программа на Ivory взаимодействует через то, что упорядочено в монадах Ivory eff
    и Module (в частности, какие бы значения ни передавались компилятору Ivory).
  • Описание CλaSH взаимодействует через аппликативный Signal (в частности, значение, называемое topEntity).

Тенденция ясна? (Нет, это не монады. Сигнал только аппликативен, и я подозреваю, что Lava ведет себя аналогично.)


Этот список охватывает наши три категории. В каждой из них создается программа (в широком смысле), просто создавая значение в Haskell. Помимо этого, единственные реальные различия это:


  • тип этого значения,
  • какая система его обрабатывает (компилятор и среда исполнения Haskell, какой-то другой компилятор и, возможно, среда исполнения или их комбинация),
  • и возможный вывод (собственный двоичный файл, битовый код LLVM, код на C, код VHDL, код на ассемблере, вход в средство проверки модели и т.д.).

Игнорирование расплывчатой природы термина «декларативный» довольно напрямую связано с декларативным характером программ на Haskell.


С этой точки зрения, Haskell все еще компилируется для запуска в какой-то встраиваемой среде. При этом компиляция может продолжаться вне системного компилятора Haskell, и запуск может не использовать его среду исполнения.