Umka: новый статически типизированный скриптовый язык
Только что вышла первая версия разработанного мной статически типизированного встраиваемого скриптового языка Umka. Он призван сочетать гибкость привычных скриптовых языков с защитой от ошибок типов на этапе компиляции в байт-код. Основная идея языка — Explicit is better than implicit — позаимствована из «дзена Python», однако должна приобрести здесь несколько иной и более очевидный смысл.
Сколь бы частными и субъективными ни были впечатления, побудившие меня взяться за разработку языка, я надеюсь, что замысел оказался не наивным. Под катом я кратко расскажу о возможностях языка и мотивах его создания.
Мотивы
Первым достоинством динамической типизации обычно называют сокращение цикла разработки/отладки и экономию времени программиста. Рискуя вызвать неудовольствие публики, должен признаться, что мой собственный опыт этого никак не подтверждает. Всякий раз после ничтожного исправления своего скрипта обучения нейросети на Python я вынужден дожидаться загрузки Python, NumPy, PyTorch, чтения большого массива данных из файлов, передачи его в GPU, начала обработки — и лишь тогда обнаруживать, что PyTorch ожидал тензор размера (1, 1, m, n, 3) вместо (1, m, n, 3).
Я охотно признаю, что многие предпочитают динамически типизированные языки по своим личным причинам. Возможно даже, что склонность или неприязнь к динамической типизации — явление того же порядка, что и отношение к оливкам и томатному соку. Попытки объективного исследования этого вопроса приводят, видимо, к неубедительным результатам.
В то же время популярность TypeScript, введение аннотаций типов в Python, горячие дискуссии на Reddit и Хабре заставляют думать, что фактическое отождествление скриптовых языков с динамически типизированными вовсе не догма, а стечение обстоятельств, и статически типизированный скриптовый язык имеет полное право существовать.
Так появился язык, названный в честь кота, названного в честь медведя.
Язык
Синтаксис языка в целом был навеян Go. С примерами синтаксических конструкций можно познакомиться на странице проекта. При объявлении переменных может использоваться сокращённая запись с выводом типов. Заслуживает внимания отступление от правил Go, сделанное в синтаксисе указателей. Создатели Go сетовали, что буквальное следование примеру C обернулось здесь излишним усложнением синтаксиса и что разумнее было бы ввести постфиксный оператор разыменования указателей вроде паскалевского p^
вместо *p
. Именно так и сделано в Umka.
Транслятор Umka компилирует в байт-код, который затем исполняется стековой виртуальной машиной. Все проверки типов делаются на этапе компиляции. Данные в стеке уже не несут никакой информации о типах. Транслятор поставляется в виде динамической библиотеки со своим API и небольшой «обёртки» — исполняемого файла. Исходный код написан на C99 и переносим на разные платформы. Сейчас выпущены сборки для процессора x86–64 (Windows и Linux).
Управление памятью пока сделано на основе счётчиков ссылок. Если язык вызовет какой-то интерес и будут попытки его использовать, появится смысл устроить более совершенный сборщик мусора. Язык поддерживает классические составные типы данных (массивы и структуры), размещаемые на стеке, и динамические массивы, размещаемые в куче. Любой классический массив или структуру можно разместить и в куче явным вызовом new()
.
Полиморфизм обеспечивается интерфейсами в стиле Go. Понятий класса, объекта и наследования нет.
Многозадачность построена на понятии «волокон» (fibers) — упрощённых потоков, запускаемых в пределах одной виртуальной машины и явно вызывающих друг друга. По сути это синонимично понятию сопрограмм (coroutines). Поскольку логика применения этих сопрограмм немного отступает от традиции Go и становится ближе к Lua и Wren, есть смысл привести образец кода:
fn childFunc(parent: std.Fiber, buf: ^int) {
for i := 0; i < 5; i++ {
std.println("Child : i=" + std.itoa(i) + " buf=" + std.itoa(buf^))
buf^ = i * 3
fibercall(parent)
}
}
fn parentFunc() {
a := 0
child := fiberspawn(childFunc, &a)
for i := 0; i < 10; i++ {
std.println("Parent: i=" + std.itoa(i) + " buf=" + std.itoa(a))
a = i * 7
if fiberalive(child) {
fibercall(child)
}
}
fiberfree(child)
}
Примеры
В качестве основного примера, демонстрирующего возможности языка, рекомендую посмотреть программу рендеринга трёхмерных сцен на основе обратной трассировки лучей. Здесь весьма органично используются рекурсия, интерфейсы, динамические массивы; несколько более искусственно — многозадачность.
Примером встраивания транслятора Umka в проект на C может послужить исходный код исполняемой «обёртки» самого транслятора. Там же есть и образец расширения языка Umka внешними функциями на C.