Funxy два месяца спустя: работа над ошибками, VM и прагматизм
- четверг, 12 февраля 2026 г. в 00:00:07
Два месяца назад я писал на Хабр о первом релизе Funxy — гибридного языка программирования. Тогда это был эксперимент по созданию своего языка с выводом типов, императивного, с функциональными возможностями. Funxy был сырой, интерпретатор мог упасть на валидном коде, производительность хромала, а некоторых привычных вещей просто не было.
С тех пор вышло несколько релизов. Мы исправили много ошибок, переписали рантайм и добавили недостающие инструменты. Хочу рассказать, что изменилось.
TLDR:
Стабильность: десятки багфиксов — падения на валидном коде, рекурсия, edge-кейсы VM
Рантайм: tree-walk интерпретатор → стековая VM (быстрее, легче по памяти)
Язык: const, return, лямбды (\x -> x + 1), list comprehensions, block syntax для DSL
Типы: strict mode, flow-sensitive typing
Тулинг: LSP и дебаггер
Embedding: встраивание Funxy в Go-приложения как скриптовый движок
Самая большая проблема первой версии — нестабильность. Интерпретатор мог упасть на валидном коде, рекурсия ломалась на глубоких вызовах, а некоторые edge-кейсы в VM приводили к непредсказуемому поведению.
За эти три месяца мы пофиксили десятки багов: падения при глубокой рекурсии, некорректная работа с замыканиями, ошибки индексирования, бесконечные циклы в парсере, проблемы с приоритетами операторов в VM. Добавили fuzz-тестирование. Язык стал заметно устойчивее — теперь можно писать что-то нетривиальное и не натыкаться на крэши.
Одним из узких мест первых версий была производительность. Tree-walk интерпретатор (который просто ходит по AST) — это просто и понятно, но медленно.
В версии 0.4.x мы переехали на стековую виртуальную машину (VM).
Что это дало:
Скорость: Вычислительные задачи стали выполняться быстрее
Память: Примитивы (Int, Float, Bool) теперь живут на стеке (используем Tagged Pointers), что сильно разгрузило Garbage Collector
Простор для дальнейших оптимизаций
Конкретные цифры (бенчмарки на Apple M3 Pro):
Тест | Tree-walk | VM | Ускорение |
|---|---|---|---|
Fibonacci(20), рекурсия | 34 ms | 4 ms | ~8x |
Higher-order (foldl/filter/map) | 5.2 ms | 0.5 ms | ~10x |
Fibonacci(20), только исполнение* | 34 ms/307k allocs | 3.7 ms/25 allocs | ~9x |
* — без учёта парсинг�� и компиляции.
Аллокации — главный выигрыш. В рекурсивных задачах VM делает 25 аллокаций вместо 307 000. Это другой порядок нагрузки на GC.
Мы добавили несколько вещей, чтобы писать код было комфортнее.
Раньше для констант использовался свой синтаксис (pi :- 3.14), а возврат из функции был только неявным (последнее выражение). Теперь можно писать так, как многие привыкли:
const maxRetries = 5 // Синтаксис maxRetries :- 5 по-прежнему доступен fun findUser(users, id) { for u in users { // Ранний выход! Раньше пришлось бы городить вложенные if-ы if u.id == id { return Some(u) } } None }
Для map, filter и прочих функциональных радостей синтаксис стал короче:
// Было map(fun(x) { x + 1 }, list) // Добавили map(\x -> x + 1, list)
Генераторы списков — удобная штука:
// Квадраты четных чисел squares = [x * x | x <- 1..10, x % 2 == 0]
Для DSL и вложенных структур добавили Block Syntax (похоже на trailing lambdas). Это синтаксический сахар: если последний аргумент функции — список выражений, скобки можно опустить. Это позволяет описывать UI или конфигурацию в декларативном стиле:
// div принимает атрибуты и список дочерних элементов (блок) fun aboutPage() { layout("About", div { h2 { text("About") } p { text("This example demonstrates:") } ul { li { text("Clean block syntax for UI components") } li { text("Nested component composition") } li { text("HTML rendering with kit/ui") } li { text("HTTP routing with kit/web") } } p { a(href: "/") { text("Back to home") } } }) }
Funxy — статически типизированный язык. Но для обычного прикладного кода аннотации типов почти не нужны. Компилятор выводит их сам:
// Типы нигде не указаны, но компилятор знает всё: // processOrders : List<{price: Float, qty: Int}> -> Float fun processOrders(orders) { orders |> filter(\o -> o.qty > 0) |> map(\o -> o.price * o.qty) |> foldl(\a, b -> a + b, 0.0) }
Из нового в системе типов:
Strict Mode: Если вам нужна большая строгость, включаете directive "strict_types" — и компилятор перестаёт пропускать потенциально опасные неявные сужения (например, когда значение типа Int | String передаётся как Int без проверки).
directive "strict_types" fun plusOne(n: Int) -> Int { n + 1 } x: Int | String = 10 // plusOne(x) // Ошибка в strict mode: нужно явно сузить тип match x { n: Int -> print(plusOne(n)) // OK _: String -> Nil }
Flow-Sensitive Typing: Внутри if компилятор понимает, что тип уточнился.
fun normalize(x: Int | String) -> String { if typeOf(x, Int) { // В этой ветке x уже Int n = x * 10 + 1 "число: " ++ show(n) } else { // А здесь x уже String "строка: " ++ x } } print(normalize(7)) // число: 71 print(normalize("abc")) // строка: abc
Неявное приведение Int к Float: Мелочь, которая экономит нервы. Если функция ожидает Float, можно спокойно передавать Int. Компилятор сам вставит инструкцию конвертации (widening), так что писать 10.0 вместо 10 больше не обязательно.
fun area(width: Float, height: Float) -> Float { width * height } // Работает: 10 и 20 (Int) приводятся к Float area(10, 20)
Реализовали Language Server (LSP) со следующими опциями:
Hover (показать тип под курсором)
Go to Definition (переход к определению)
Diagnostics (подсветка ошибок на лету)
Появился Debugger. Для пошаговой отладки вместо ручных print можно запустить скрипт с флагом -debug и пройтись по шагам, посмотреть переменные и понять, где именно вы ошиблись.
Одна из ключевых возможностей — встраивание. Язык написан на Go, и его легко интегрировать в ваше Go-приложение.
Зачем? Чтобы вынести бизнес-правила, конфигурацию или сценарии обработки данных в скрипты, которые можно менять без пересборки основного бинарника.
// Go vm := funxy.New() // Биндим структуру Go, чтобы она была видна в скрипте vm.Bind("user", &User{Name: "Alice", Balance: 100}) // Исполняем скрипт vm.Eval(` if user.Balance > 50 { user.Name = "Rich " ++ user.Name } `) // Выполняем функцию Funxy из Go vm.Call("process_user", "Alice", 50)
Можно вызывать функции Funxy из Go и наоборот. Работает прозрачно.
Скрипты и автоматизация. Один бинарник, ноль зависимостей. В стандартной библиотеке — HTTP, gRPC, JSON, protobuf, SQL, CSV, работа с битами и байтами. Удобно для CLI-утилит, пайплайнов обработки данных и одноразовых скриптов
Прототипирование бэкенд-логики. Вывод типов убирает boilerplate, но компилятор всё равно ловит ошибки до запуска. Быстро набросать API-обработчик, проверить идею — и при этом не остаться без типобезопасности
Встраивание (Embedding). Использование Funxy как скриптового движка внутри Go-приложений. Бизнес-правила, маршрутизация запросов, конфигурации — всё, что хочется менять без пересборки
Проект жив и развивается. Если вам интересно попробовать что-то новое — заглядывайте.
Ссылки:
Будем рады любым issues, пулл-реквестам и просто фидбеку в комментариях. Что сломалось? Чего не хватает? Пишите, пожалуйста.