Современный мир встроенных систем и программирования тесно связан с поиском инструментов, которые объединяют выразительность высокоуровневых языков с производительностью низкоуровневого кода. В этом контексте компилятор Lisp, способный переводить Lisp-функции непосредственно в машинный код ARM, представляет собой уникальное и перспективное решение. Компилятор, разработанный на основе uLisp и написанный на самом Lisp, становится важным шагом на пути к эффективной реализации кода на маломощных устройствах с архитектурой ARM Cortex. uLisp изначально задумывался как компактная реализация Lisp для микроконтроллеров. Добавление в него функционала компилятора ARM выделяет проект среди аналогов, поскольку позволяет создавать машинные коды, максимально оптимизированные для определенной аппаратной платформы.
Вторая версия компилятора стала значительно усовершенствованной за счет включения поддержки локальных переменных с конструкциями let и let*, организации циклов через loop, dotimes и return, а также дополнительных условных операторов when и unless. Пользователь теперь имеет богатый инструментарий, позволяющий писать более сложные функции с высокой эффективностью. Особенностью данного компилятора является его тесная интеграция с инструкциями ARM Thumb-2, которые позволяют работать с большими значениями за счет команд movw и movt, поддерживают целочисленное деление и операции модуля благодаря sdiv и mls, а также снабжены компактными управляющими инструкциями cbz, cbnz и it. Такая оптимизация делает машинный код максимально компактным и быстрым, в то время как компилятор остается простым в использовании и разработке. Стоит отметить, что благодаря использованию Thumb-2, новая версия компилятора предназначена исключительно для ARM-процессоров серии M4, M33 и новее.
Это ограничивает возможность применять его на более старых платформах с M0 и M0+, таких как популярные ATSAMD21, RP2040 или nRF51822. Тем не менее, современные микроконтроллеры, такие как ATSAMD51, RP2350, nRF52840 или RA4M1, полностью поддерживаются и позволяют экспериментировать с производительным Lisp-кодом. Процесс работы компилятора удобен и интуитивно понятен. Пользователю достаточно вызвать функцию compile с передачей имени функции Lisp, чтобы она была преобразована в ARM-машинный код и заменила оригинальную версию. Есть возможность просмотреть сгенерированный машинный код, не выполняя его, с помощью функции comp, что помогает анализировать и оптимизировать рабочие программы.
Для эффективного управления памятью рекомендуется выполнять освобождение ранее скомпилированных функций из памяти через makunbound, что предотвращает исчерпание кода и облегчает компиляцию новых функций. В случае необходимости объём памяти, выделяемый под машинный код, можно увеличить, отредактировав директиву CODESIZE при загрузке uLisp на плату. Уникальной особенностью архитектуры компилятора является использование стека для передачи значений между выражениями и оператором во время компиляции и выполнения. Такой подход минимизирует необходимость отслеживания состояния регистров и позволяет избегать конфликтов при работе со сложными выражениями, в том числе рекурсивными. Для работы с регистрами ARM применена следующая стратегия: r0-r3 используются для передачи параметров функции, r0 возвращает результат работы, а r4-r7 служат для сохранения копий аргументов и локальных переменных.
Во время компиляции выражений значения последовательно помещаются на стек, кроме последнего, который остается в r0, после чего вызывается соответствующая операция. Это упрощает генерацию корректного и оптимального кода для выражений, таких как умножение, сложение, а также более сложные конструкции. Данный компилятор поддерживает рекурсивные вызовы прямо из тела функции. Чтобы избежать перезаписи регистров с параметрами и локальными переменными, они сохраняются на стеке до начала рекурсивного вызова и восстанавливаются после его завершения. Это расширяет возможности реализации сложных алгоритмов в Lisp, позволяя разработчикам использовать преимущества рекурсии при сохранении высокой производительности.
Особое внимание уделено типам возвращаемых значений. Для булевых значений nil и t применяются значения 0 и 1 соответственно. Чтобы избежать ошибок и неоднозначностей при смешении типов булевых и целочисленных значений в выражениях, компилятор вводит механизм отслеживания типа возвращаемого значения - :integer или :boolean. Это позволяет точно контролировать корректность кода и предупреждать о потенциальных ошибках, связанных с неверным использованием типов. Приведённые примеры кода демонстрируют практическую ценность и преимущества использования компилятора.
Функция нахождения наименьшего простого делителя числа factor реализована с использованием let, loop, return, when и операции mod. Компиляция ускоряет её выполнение более чем в 200 раз, что существенно влияет на эффективность вычислений. Рассматривается и NP-трудная задача подмножественной суммы, где рекурсивная функция subsetsum-p исследует возможность достижения заданной суммы путём сложения подмножества элементов списка. Компилированный код сокращает время выполнения с десятков секунд до долей секунды, что открывает новые горизонты при решении сложных задач на ограниченных ресурсах. Важным примером является и функция возведения в степень iex, которая оптимально применяет побитовый сдвиг и проверку чётности для ускоренного возведения в степень, и функция reversedigits, способная быстро обращать цифры числа, используя циклы и арифметические операции.
Интересен также пример рекурсивной реализации чисел Фибоначчи, которая благодаря компиляции работает существенно быстрее оригинальной версии на Lisp. В дополнение сгенерированный код можно проанализировать для понимания того, как именно машинные инструкции используются для реализации рекурсии и арифметики на низком уровне. Компилятор создан на основе идей, вдохновлённых известным трудом Питера Норвига "Парадигмы искусственного интеллекта в программировании", где подробно рассматриваются методы создания компиляторов и генерации кода для виртуальных и физических машин. Такой подход, при котором Lisp-компилятор пишет сам Lisp, а конечный код исполняется непосредственно на ARM, отражает мощь Lisp как языка с максимальной метапрограммной выразительностью. Интеграция с ARM-ассемблером в uLisp расширяет возможности разработчиков, предоставляя приемлемую среду разработки, отладки и выполнения эффективного машинного кода на маломощных платформах.
Популяризация таких инструментов повышает интерес к применению Lisp в embedded-разработке, где ранее доминировали низкоуровневые языки и среда без динамической компиляции. В сравнении с современными технологиями, такими как WebAssembly, где ограничена возможность динамического кодогенерации и исполнения из-за изолированности кода и данных, подход на основе uLisp и ARM Thumb-2 предлагает реальную возможность получать высокопроизводительный машинный код на лету и применять его для ресурсов ограниченного устройства. Это делает разработки на Lisp не только познавательными, но и практически оправданными. Прогресс в данном направлении открывает дорогу не только для дальнейшего усовершенствования производительности Lisp-программ, но и для расширения языка новыми синтаксическими конструкциями и возможностями, приближающими его к полноценным языкам программирования для embedded-устройств. Обширная поддержка условных операторов, циклов, арифметических функций и логических предикатов указывает на зрелость разработки и готовность к практическому использованию.
Подводя итог, компилятор Lisp для ARM, написанный на Lisp, представляет собой уникальное сочетание простоты, эффективности и гибкости. Это не просто учебный проект - это работающий инструмент, позволяющий использовать потенциал ARM-платформ в сочетании с мощью Lisp. Благодаря развитию этого компилятора экспоненты программирования для микроконтроллеров становятся более выразительными и быстрыми, что открывает большую перспективу для разработчиков и исследователей в области embedded-систем и языковых технологий. .