Umka: новый статически типизированный скриптовый язык

dqugmfwxgaoxca2y4b5rtckcpyu.png
Только что вышла первая версия разработанного мной статически типизированного встраиваемого скриптового языка 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.

image

© Habrahabr.ru