Глубина, скорость, красота — в поисках философского камня
Зачем мне ещё один язык программирования
Проект: Алатырь
Каждый новый проект начинается для меня с одного и того же вопроса: на чём писать. И каждый раз ответ оказывается компромиссом.
Нужна система реального времени, где у каждой операции свой бюджет, где сборщик мусора не имеет права «остановить мир» в неподходящий момент и где временем и ресурсами управляю я, а не среда выполнения, — и я беру язык, в котором есть все рычаги, но писать на нём долго и больно. Нужно быстро собрать что-то большое и живое — беру язык с удобными абстракциями и плачу за удобство тем, что перестаю понимать, что на самом деле делает машина. Нужно, чтобы работало быстро, — ищу середину и снова чем-то жертвую. А в большом проекте всё это часто склеивается в несколько языков сразу, и швы между ними становятся отдельной работой.
По сути, выбор стека — это выбор не языка, а того, какой из трёх вершин пожертвовать.
Треугольник выбора
Глубина. Сколько машины мне доступно. Могу ли я дойти до любой технической точки оборудования — до бита, до конкретной инструкции процессора — не выходя из языка.
Скорость. Насколько эффективна программа на самой машине. Сколько стоит каждая строка в тактах и байтах, и нет ли платы, о которой я не просил.
Красота. Насколько приятно и удобно на языке думать и писать: сильные абстракции, узнаваемый синтаксис, код, который хочется перечитывать.
Знакомая формула «выбери два из трёх» здесь работает почти безотказно. Глубина и скорость — но тогда без красоты. Красота — но тогда без глубины, а часто и без скорости.
Алатырь — моя попытка не выбирать. Выкрутить все три вершины на максимум и не жертвовать ни одной. Чтобы, начиная проект, не задумываться, хватит ли языку рычагов, и не бросать его на полпути из-за неудобства. По замыслу — от загрузчика и систем реального времени до веба и игр.
Звучит как поиск философского камня.
Так оно и есть.
Глубина: до бита и до такта
Потолок выразительности Алатыря — ассемблер. Всё, что можно сказать на уровне машины, можно сказать и на Алатыре.
Ассемблер здесь не вставка из чужого языка в кавычках, а часть самого языка: типизированные инструкции, метки, переходы. Сами типы построены над моделью машины: в основании лежат сырые блоки битов, а целые со знаком, без знака и числа с плавающей точкой — лишь способы эти биты читать.
Но глубина — это не только биты. Это ещё время и ресурсы. В Алатыре нет сборщика мусора, а значит, нет и пауз, когда программа замирает, пока среда выполнения наводит порядок в памяти. Каждое выделение памяти явное и идёт через аллокатор, который назван в коде: арена, регион, свой собственный. Освобождение тоже явное — через defer, без деструкторов, срабатывающих где-то за кадром. Для системы реального времени это главное: в программе не происходит ничего, что я не написал сам или что не задокументировано в спецификации языка.
И нет лестницы «уровней», где для работы с железом нужно спуститься в отдельный, урезанный диалект. Язык один. Если какому-то модулю нужны строгие гарантии — без аллокаций, без абстракций, без стандартной библиотеки, — он объявляет ограничения, и компилятор их проверяет. «Только ассемблер» — это не другой язык, а одно из таких ограничений.
Скорость: без скрытой платы
Алатырь компилируется заранее, прямо в машинный код, через ассемблер самой платформы. Рядом с программой не живёт среда выполнения, а в самой программе нет скрытых копий, неявных вызовов и диспетчеризации, о которой не написано в коде.
Цифр я здесь не привожу сознательно. Бенчмарки в проекте есть, но число, которое нельзя воспроизвести на своей машине, — это реклама, а не измерение. Цифры будут, когда их можно будет честно проверить. Пока скорость обоснована устройством: в программе нет ничего, за что платишь, не зная об этом.
Красота: сильные абстракции, узнаваемый синтаксис
Мне хотелось, чтобы язык с потолком ассемблера читался как современный язык высокого уровня. Главный ход — одна форма объявления для всего: имя, тип, значение.
Point := struct { x : u64, y : u64 }
add := fn(a : u64, b : u64) -> u64 { a + b }
Vec := fn(T : type) -> type { struct { ptr : ptr(T), len : usize } }
mut count : u64 = 0
Структура, функция и обобщённый тип объявляются одинаково. Обобщённый тип Vec — это просто функция, которая работает во время компиляции и возвращает тип. Отдельного механизма шаблонов нет; есть одна идея, доведённая до конца.
Та же мысль — в настройке проекта. Манифест пакета пишется не на отдельном языке конфигураций, а на самом Алатыре: цели сборки, архитектуры, ограничения — всё это обычные значения, которые компилятор вычисляет.
Почему вершины не тянут друг друга вниз
Вершины треугольника обычно конфликтуют не случайно. Абстракция удобна потому, что прячет детали, — и именно в спрятанном живёт непредсказуемая цена. Глубина неудобна потому, что всё приходится говорить явно.
Алатырь держится на одном принципе: программист всегда знает, что делает машина. Каждая конструкция языка превращается в машинный код по описанным в спецификации правилам — так, что результат можно предсказать, не запуская компилятор. Абстракция не стоит дороже кода, который она заменяет.
Отсюда и связь вершин. Раз абстракция прозрачна, красота не отнимает скорость. Раз спуститься до бита можно из любого места, удобство не закрывает глубину. Красота не прячет цену — она только избавляет от необходимости писать её руками.
Из того же принципа следует, пожалуй, самое строгое правило языка: в нём нет неопределённого поведения. Операция либо выполняется корректно, либо детерминированно останавливает программу с внятной ошибкой. Если нужна голая скорость без проверок, её включают явно, для конкретного участка кода:
unchecked {
while j * j <= n {
if n % j == 0 { return false }
j += 1
}
}
Внутри unchecked поведение определяется железом — переполнение идёт по правилам процессора, — но никогда не тем, «как удобнее компилятору». И компилятор не имеет права включить этот режим сам.
Где стоят соседи
Существующие языки я вижу не как неудачные попытки, а как разные расстановки приоритетов.
C и ассемблер выбрали глубину и скорость. Это честные языки: они дают всё. Но за неопределённое поведение в C расплачиваешься тем, что оптимизатор вправе сломать код, который выглядит правильным.
Языки со сборщиком мусора и средой выполнения выбрали красоту и скорость разработки. Они прекрасны для своего круга задач — и расплачиваются глубиной: до машины из них не дотянуться, а паузы и цена абстракций непредсказуемы.
Ближе всего к Алатырю — Rust и Zig, у которых я многому учусь. Rust сделал безопасность памяти обязательной через проверку заимствований; я выбираю модель попроще — явное освобождение через defer, арены, регионы. Zig близок мне явностью и вычислениями во время компиляции; мне хочется сделать ещё шаг — чтобы неопределённого поведения не было вовсе и чтобы ассемблер был не вставкой, а частью языка.
Почему Алатырь
В славянских преданиях Алатырь — «всем камням отец», неподвижный камень в центре мира, от которого всё отмеряется. Для языка, в основе которого машина, которую никогда не прячут, мне трудно представить имя точнее. Точка отсчёта в Алатыре — машина.
Где проект сейчас
Честно о статусе. Компилятор Алатыря написан на самом Алатыре и собирает сам себя, причём побайтово воспроизводимо: каждая сборка даёт тот же самый двоичный файл. Это работает на x86_64 под Linux, без стандартной библиотеки C. Спецификация языка написана целиком, но пока остаётся черновиком на рецензии. Компилятор — предварительная версия.
Бэкенды для aarch64, riscv64 и WebAssembly уже есть, но умеют пока подмножество языка. Веб через WebAssembly — направление, а не готовая возможность. Принцип «корректно или внятная ошибка» — цель, к которой проект идёт, и найденные отступления от неё записаны как ошибки, которые нужно исправить.
Алхимики философского камня так и не нашли. Но по дороге открыли химию. Я готов к любому из этих исходов…