Язык Terra — низкоуровневый партнёр Lua

Язык Terra — низкоуровневый партнёр Lua

Terra — низкоуровневый язык системного программмирования, встраиваемый и имеющий возможность метапрограммирования с помощью языка Lua.

Как и C/C++, язык Terra статически типизируемый, компилируемый язык с «ручным» управлением памятью. В отличие от C/C++, он изначально спроектирован для метапрограммирования с помощью Lua.

Terra спроектирован на основе того факта, что C/C++ на самом деле состоит из множества “языков.” Есть ядро языка, состоящее из операторов, управления потоком исполнения и вызова функций, но окружающий его язык, это метаязык, составленный из смеси разных вещей, таких, как препроцессор, шаблоны, объявления структур. Шаблоны сами по себе образуют Тьюринг-полный язык и используются для порождения оптимизированных библиотек, таких, как Eigen, но ужасны в плане практического использования.

В языке Terra мы отказались от идеи сделать метаязык C/C++ более мощным и заменили его настоящим языком программирования, Lua.

Комбинация низкоуровневого языка, который может метапрограммироваться высокоуровневым скриптовым языком делает возможными многие варианты поведения, невозможные в других системах. В отличие от C/C++, код на Terra может быть JIT-компилирован и запущен совместно с интерпретатором Lua, что делает простым написание библиотек, зависящих от рантаймовой кодогенерации.

Возможности других языков, такие, как условная компиляция и шаблоны просто проигрывают по сравнению с использованием Lua для метапрограммирования Terra:

Вы можете использовать Terra и Lua как…

Встраиваемый JIT-компилятор для конструирования языков. Мы используем техники многоступенчатого программирования [2] для того, чтобы сделать возможным метапрограммирование Terra с использованием Lua. Выражения, типы и функции языка Terra являются значениями первого класса в языке Lua, что делает возможным генерацию произвольных программ в рантайме. Это позволяет вам компилировать предметно-ориентированные языки (DSL), написанные на Lua в высокопроизводительный код на Terra. Более того, так как Terra построен на экосистеме Lua, легко встроить программу на Terra-Lua в другую программу в виде библиотеки. Такой дизайн позволяет вам добавлять JIT-компилятор в ваше существующее программное обеспечение. Вы можете использовать его для добавления JIT-компилируемых DSL-языков в ваше приложение, либо автоматически и динамически конфигурировать высокопроизводительный код.

Скриптовой язык с высокопроизводительными расширениями. Хотя производительность Lua и других динамических языков непрерывно улучшается, низкой уровень абстракции даёт вам предсказуемое управление производительностью, когда вам это нужно. Программы Terra используют тот же бэкенд LLVM, который Apple использует в своих компиляторах C. Это значит, что производительность кода Terra близка к аналогичному коду C. Например, наши переводы программ nbody и fannhakunen из бенчмарка [1] языков программирования имеют производительность, отличающуюся не более, чем на 5% от их эквивалентов на С, скомпилированных на Clang, фронтенде LLVM. Terra также включает встроенную поддержку SIMD-операций и другие низкоуровневые возможности, такие, как запись и предвыборка не-временной памяти. Вы можете использовать Lua для организации и конфигурирования вашего приложения, а затем, когда вам нужна управляемая производительность, сделать вызов кода Terra.

Самостоятельный низкоуровневый язык. Terra спроектирован так, что может работать независимо от Lua. Фактически, ваша конечная программа не требует Lua, вы можете сохранить код Terra в файл .o или в исполняемый файл. Вдобавок к ясному разделению между высокоуровневым и низкоуровневым кодом, такой дизайн позволяет вам использовать Terra как независимый низкоуровневый язык. В таком сценарии использования, Lua выступает в роли мощного языка метапрограммирования. Lua служит заменой шаблонов C++ [3] и макросов препроцессора C (X-Macro) [4], имея при этои лучший синтаксис и лучшие свойства в плане гигиены [5]. Так как Terra существует только как код, встроенный в метапрограмму Lua, те возможности, которые обычно встроены в низкоуровневый язык, могут быть реализованы как библиотеки Lua. Такой дизайн сохраняет ядро Terra простым, делая возможным сложное поведение, такое, как условная компиляция, пространства имён, шаблоны, и даже систему классов, реализованную в виде библиотек.

Чтобы получить больше информации об использовании Terra, смотрите руководство для начинающих и справочник по API. Наши публикации дают более глубокое представление о дизайне языка.

Порождающее программирование

Сущности языка Terra, такие, как функции, типы, переменные и выражения являются в Lua значениями первого класса, они могут быть сохранены как переменные, а также передаваться в функции Lua и возвращаться из функций Lua. Используя конструкции из многоступенчатого программирования, вы можете писать код на Lua, порождающий произвольный код Terra.

Многоступенчатые операторы

В коде Terra вы можете использовать опрератор escape ([]), помещающий результат выражения Lua в код Terra:

Значение escape вычисляется, когда функция Terra компилируется, и результат помещается в код Terra. В данном примере, это означает, что выражение math.sin(5) будет вычислено один раз, и код, реализующий функцию Terra, возвратит константу. Это можно проверить, если вывести скомпилированную версию функции sin5.

Escape-операторы также могут возвращать другие сущности Terra, например, функции:

В этом случае, код Terra будет вставлен в функцию Terra, сохранённую в переменной add4:

Фактически, любое имя, используемое в коде Terra, такое, как add4 или foo.bar рассматривается по умолчанию, как если бы оно было escape-оператором.

Внутри escape-оператора вы можете ссылаться на переменные, определённые в Terra:

Так как escape-операторы вычисляются до того, как функции Terra скомпилированы, переменные a и b не будут иметь конкретных целых значений внутри escape-оператора. Вместо этого, внутри кода Lua переменные a и b являются символами Terra, представляющими ссылки на значения Terra. Так как choosesecond возвращает символ b, функция в примере возвратит значение переменной b кода Terra, когда она будет вызвана.

Оператор цитирования (quotation), обратный апостроф, позволяет вам генерировать операторы и выражения Terra в Lua. Они могут быть вставлены в код Terra с использованием escape-оператора.

Для генерации операторов вместо выражений исользуйте опретор quote:

Компиляция языка

С помощью этих двух операторов вы можете генерировать произвольный код на Terra во время компиляции. Это делает комбинацию Lua/Terra хорошо подходящей для написания компилятора высокопроизводительного предметно-ориентированного языка. Например, мы можем реализовать компилятор BF, минимального языка, эмулирующего машину Тьюринга. Функция compile на языке Lua принимает строку кода BF и максимальный размер ленты N. Затем она генерирует функцию Terra, реализующую BF-код. Это «скелет», который подготавливает программу BF:

Функция body отвечает за генерацию тела программы BF по строке кода:

Цикл проходит по строке кода, генерирует соответствующий код на Terra для каждого символа BF (например, ">" сдвигает ленту на один символ и реализуется на Terra кодом ptr = ptr + 1). Сейчас мы можем скомпилировать функцию BF:

Результат, add3 — функция Terra, прибавляющая 3 к входному символу и выводящая результат:

Также мы можем использовать оператор goto (goto labelname) и метки (::labelname::) для реализации конструкции цикла в BF:

Мы используем конструкции порождающего программирования для реализации предметно-ориентированных языков и автонастройки. Наша статья в PLDI описывает нашу реализацию Orion, языка для ядер обработки изображений, и мы в процессе портирования языка Liszt (основанное на сетках решение дифференциальных уравнений в частных производных) на язык Terra.

Встраивание и взаимодействие

Языки программирования не существуют в вакууме, и возможности порождающего программирования в Terra могут быть полезны даже в проектах, которые изначально реализованы на других языках программирования. Мы делаем возможным интеграцию Terra с другими проектами, так что вы можете использовать генерацию низкоуровнего кода, и в то же время большая часть вашего проекта будет реализована на каком-либо традиционном языке.

Сначала сделаем возможной передачу значений между Lua и Terra. Наша реализация построена на основе интерфейса «чужих» функций (foreign function) LuaJIT. Вы можете вызвать функции Terra прямо из Lua (и наоборот) и получать доступ к объектам прямо из Lua (более подробно описано в справочнике по API).

Более того, Lua-Terra обратно совместим с чистыми Lua и C, что облегчает использование существующего кода. В Lua-Terra, вы можете использовать require или loadfile и рассматривать файл как программу Lua (используйте terralib.loadfile для загрузки комбинированного файла Lua-Terra). Вы можете использовать terralib.includec для импорта функций C из существующих заголовочных файлов.

Наконец, Lua-Terra может также быть встроен в существующме приложения путём линковки приложения с libterra.a и использования Terra’s C API. Интерфейс очень похож на интерфейс интерпретатора Lua. Простой пример инициализирует Terra и запускает код из файла, определённого в каждом аргументе:

Простота

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

Условная компиляция

Как правило, условная компиляция совершается с использованием директив препроцессора (например, #ifdef), или какой-либо системы сборки. При использовании Lua-Terra, мы можем написать код Lua, определяющий, как сконструировать функцию Terra. Так как Lua является полноценным языком программирования, он может делать вещи, которые большинство препроцессоров делать не могут, например, вызывать внешние программы. В этом примере, мы применяем условную компиляцию, чтобы скомпилировать функцию Terra по-разному для OSX и Linux путём вызова uname, чтобы определить операционную систему, и заием используем оператор if для инстанцирования разных версий функции Terra в зависимости от результата:

Пространства имён

Статически типизированным языкам обычно нужны конструкции, которые решают проблему пространств имён (например, ключевое слово namespace в C++, или конструкция import в Java). Для Terra мы просто используем таблицы первого класса из Lua как способ организации функций. Когда вы используете любое имя, например, myfunctions.add, внутри функции Terra, Terra будет разрешать его во время компиляции в связанное с ним значение Terra. Вот пример размещение функции Terra внутри таблицы Lua, с последующим вызовом из другой функции Terra:

Фактически, вы уже видели такое поведение когда мы импортировали функции С:

Функция includec просто возвращает таблицу Lua ( C ), содержащую функции C. Так как C — это таблица Lua, вы можете делать итерации по ней:

Шаблоны

Так как типы и функции Terra являются значениями первого класса, вы можете получить фунциональность, близкую к шаблонам C++, просто создав тип Terra и определив функцию Terra внутри функции Lua. Ниже приведён пример, в котором мы определяем функцию Lua MakeArray(T), приимающую тип T языка Terra и порождающую объект Array который может хранить множество объектов типа T (т.е. простую версию std::vector из C++).

Как показано в примере, Terra позволяет вам определять методы в типах struct. В отличие от других статически типизированных языков с классами, здесь нет встроенных механизмов наследования или run-time полиморфизма. Декларации методов, это просто синтаксический сахар, который ассоциирует таблицы методов Lua с каждым типом. Здесь метод get эквивалентен следующему:

Объект ArrayT.methods в таблице Lua хранит методы для типа ArrayT.

Аналогично, вызов, например, ia:get(0) эквивалентен T.methods.get(&ia,0).

Специализация

Помещая функцию Terra внутрь функции Lua, вы можете скомпилировать разные версии функции. Здесь мы генерируем разные версии функции степени (т.е. pow2, pow3):

Система классов

Как показано в примере для шаблонов, Terra позволяет определять методы для типов struct, но не предоставляет встроенного механизма для наследования или полиморфизма. Вместо этого, обычная система классов может быть написана как библиотека. Например, пользователь может написать:

📎📎📎📎📎📎📎📎📎📎