Два месяца назад я писал на Хабр о первом релизе 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-тестирование. Язык стал заметно устойчивее — теперь можно писать что-то нетривиальное и не натыкаться на крэши.
Изменения под капотом: Stack-based VM
Одним из узких мест первых версий была производительность. 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.
Язык: ближе к общепринятому
Мы добавили несколько вещей, чтобы писать код было комфортнее.
1. const и return
Раньше для констант использовался свой синтаксис (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 }
2. Лаконичные лямбды
Для map, filter и прочих функциональных радостей синтаксис стал короче:
// Было map(fun(x) { x + 1 }, list) // Добавили map(\x -> x + 1, list)
3. List Comprehensions
Генераторы списков — удобная штука:
// Квадраты четных чисел squares = [x * x | x <- 1..10, x % 2 == 0]
4. Block Syntax
Для 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)
Инструменты: LSP и дебаггер
Реализовали Language Server (LSP) со следующими опциями:
Hover (показать тип под курсором)
Go to Definition (переход к определению)
Diagnostics (подсветка ошибок на лету)
Появился Debugger. Для пошаговой отладки вместо ручных print можно запустить скрипт с флагом -debug и пройтись по шагам, посмотреть переменные и понять, где именно вы ошиблись.
Встраивание в Go (Embedding)
Одна из ключевых возможностей — встраивание. Язык написан на 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, пулл-реквестам и просто фидбеку в комментариях. Что сломалось? Чего не хватает? Пишите, пожалуйста.
