Привет! Я написал Тулпар (Тұлпар — крылатый конь из казахской мифологии) — язык программирования, в котором ключевые слова, сообщения об ошибках и встроенные функции на казахском, синтаксис похож на Python, а программы компилируются в нативный машинный код x86-64.
Это не транспилятор в C и не обёртка над LLVM. Весь путь от исходника до ELF-файла — мой: лексер, парсер, вывод типов, оптимизатор, аллокатор регистров, кодогенератор, а также рантайм со сборщиком мусора. Рантайм не использует libc и ходит в ядро системными вызовами напрямую.
Ниже — как это устроено, что получилось по скорости, и что дальше.
Как это выглядит
анықта фиб(n: бүтін) -> бүтін:
егер n < 2:
қайтар n
қайтар фиб(n — 1) + фиб(n — 2)
әр і ішінде аралық(10):
жаз(f"фиб({і}) = {фиб(і)}")
Для знающих Python соответствие почти буквальное:
|
Tulpar |
Python |
|
Tulpar |
Python |
|---|---|---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Идентификаторы — полноценный Unicode, так что переменные и функции можно называть как угодно, включая казахские буквы ә ғ қ ң ө ұ ү һ і.
Несколько свойств языка:
— Статическая типизация с выводом типов. x = 5 - это бүтін (int64), x = 2.5 - нақты (float64). Типы параметров функций пишутся явно, тип возврата выводится (для рекурсивных функций его нужно указать). — Классы с одиночным наследованием и виртуальными методами, кортежи с возвратом нескольких значений, генераторы списков, f-строки, лямбды (без замыканий), функции как значения. — Коллекции: тізім[T], сөздік[K, V], жиын[T]. Словарь сохраняет порядок вставки. — Строки — UTF-8, но индексация и срезы идут по символам, а не по байтам: ұзындық("Қазақстан«) == 9. Для казахского текста это принципиально: «Қ», «ұ», «ы» в UTF-8 занимают по два байта, и len() в стиле C дал бы бессмыслицу. — Две модели памяти. По умолчанию — сборщик мусора. Но внутри блока қолмен: («вручную») доступны указатели, бөл(T, n)/босат(p) и арифметика указателей. Вне этого блока указателей в языке нет.
қолмен:
p = бөл(бүтін, 1000) # *бүтін, обнулён, GC им не управляет
әр і ішінде аралық(1000):
p[і] = і * і
жаз(p[10], *p)
босат(p) # освободить нужно самому
Сообщения об ошибках — тоже на казахском, и при падении программы печатается стек вызовов с файлом, строкой и функцией:
Шақырулар тізбегі (соңғысы төменде):
«prog.tlp», 16-жол, <main>
«prog.tlp», 13-жол, сыртқы
«prog.tlp», 7-жол, тереңірек
Қате: нөлдік көрсеткішке (жоқ) қатынау
Конвейер компиляции
Компилятор — около 10 тысяч строк на Rust (без интерпретатора), интерпретатор — ещё около 1,7 тысячи, рантайм — около 3,6 тысячи. Всего в репозитории ~15 тыс. строк Rust. Конвейер классический:
исходник .tlp
│ lexer.rs токены с учётом отступов, f-строки, Unicode-идентификаторы
│ parser.rs рекурсивный спуск → AST
│ check.rs проверка и вывод типов, AST → HIR (типизированное дерево)
│ lower.rs HIR → трёхадресный IR: виртуальные регистры, базовые блоки
│ opt.rs упрощение CFG, свёртка констант, copy propagation,
│ слияние mov, удаление мёртвого кода, if-конверсия (cmov)
│ inline.rs инлайнинг маленьких функций
│ regalloc.rs linear scan, веса по глубине циклов, callee/caller-saved
│ codegen.rs x86-64, синтаксис Intel, System V ABI
▼
`as` + `ld` (+ libtulpar_rt.a) → статический ELF
Рантайм без libc
Рантайм (runtime/) - это #![no_std] крейт на Rust, собираемый в статическую библиотеку. Точка входа _start лежит в нём же, системные вызовы (write, mmap, socket, …) делаются инструкцией syscall через inline-ассемблер. В итоге готовая программа — статический ELF без динамических зависимостей; hello world занимает порядка 30 КБ.
Рантайм при старте:
— выделяет под стек 1 ГБ виртуальной памяти (физическая выдаётся ядром по мере использования) с защитной страницей снизу, поэтому бесконечная рекурсия превращается в понятное «переполнение стека», а не в segfault; — ставит обработчики сигналов: обращение к защитной странице стека превращается в сообщение о переполнении стека, а разыменование нулевой ссылки (жоқ) - в сообщение со стеком вызовов; — инициализирует GC.
Сборщик мусора
GC — консервативный mark-sweep:
— корни — стек и глобальные переменные; — куча разбита на spans по размерным классам (в духе tcmalloc/Go), поэтому аллокация мелких объектов — это снятие элемента со списка свободных; — понимает внутренние указатели (указатель в середину объекта удерживает объект); — объекты без указателей (строки, тізім[бүтін] и т. п.) вообще не сканируются; — 8-байтные слова объектов сканируются консервативно, то есть точной карты указателей нет. Цена — теоретическая возможность, что целое число, похожее на адрес, удержит мусор. На практике это редко, но это честное ограничение.
Объекты устроены просто: строка — [длина в байтах][число символов][байты], список — [длина][ёмкость][данные][флаги], поля класса — по 8 байт. В иерархии классов нулевое слово — id класса, виртуальные вызовы идут через vtable[id].
Отдельная фишка — ссылки на GC-объекты можно хранить в «ручной» памяти: бөл(мәтін, 4) или бөл(Класс, 4). GC считает такие блоки корнями. Благодаря этому ручное управление не ломает автоматическое.
жина() запускает сборку принудительно, жад() возвращает размер кучи. В репозитории есть тест hello_trees.tlp: ветвистые деревья — долгоживущее на 2¹⁸ узлов и сорок временных по 2¹⁶ - и проверка, что долгоживущее после всего цела.
Производительность
Замеры на Linux x86-64, 8 ядер. Rust — rustc -O, C — gcc -O2, Python — CPython 3.14. Время одного запуска:
|
тест |
Tulpar |
Rust |
C |
Python 3 |
|---|---|---|---|---|
|
Коллатц, 3 млн чисел |
0,59 с |
0,34 с |
0,47 с |
33 с |
|
решето Эратосфена, 2·10⁷ |
0,13 с |
0,11 с |
0,10 с |
2,2 с |
|
цикл с float, 10⁸ |
0,13 с |
0,12 с |
0,13 с |
18 с |
|
|
1,55 с |
0,65 с |
0,39 с |
36 с |
Выводы:
— на простых циклах Тулпар вплотную к C и Rust; — от CPython он быстрее в 17-140 раз — ожидаемо для компилируемого языка со статическими типами против интерпретатора; — на рекурсии я проигрываю C и Rust в ~2,4 раза, на ветвистом коде — в ~1,7 раза. Причины понятны: нет раскрутки рекурсии, нет векторизации, cmov выбирается консервативно; — это не «обогнал C». Это «простой компилятор с нормальным аллокатором регистров даёт скорость того же порядка, что и промышленные».
Что даёт основной выигрыш: linear scan с учётом глубины циклов (горячие переменные получают регистры первыми), инлайнинг мелких функций, свёртка констант и if-конверсия в cmov.
Как проверять корректность оптимизатора
-
Тесты (
tests/run_tests.sh, сейчас 75 проверок):.tlp-программы с эталонными.out, каждая выполняется и с оптимизацией, и в режиме-O0, а затем ещё и в интерпретаторе. Отдельный каталогtests/errors/содержит программы, которые обязаны упасть с определённым сообщением. -
Дифференциальный фаззер (
tools/fuzz.py): генерирует случайные арифметические программы с функциями, делением, сдвигами, тернарными выражениями и считает эталон на Python (с ручной эмуляцией 64-битного переполнения). Результаты сравниваются с-O0и с оптимизированной сборкой. Расхождение — баг. -
Предупреждения компилятора считаются багами:
cargo build —releaseне должен печатать ничего.
Сеть и «настоящие» программы
Чтобы язык не оставался игрушкой для чисел, встроены TCP-сокеты, файлы и аргументы командной строки. Вот минимальный HTTP-сервер целиком на Тулпаре:
сервер = тыңда_локал(8080)
әзірше рас:
клиент = қабылда(сервер)
сұраныс = желі_оқы(клиент)
дене = «Сәлем!»
желі_жаз(клиент, f"HTTP/1.1 200 OKrnContent-Length: {дене.байт_саны()}rnrn{дене}«)
желі_жап(клиент)
Ограничения тут жёсткие: только TCP/IPv4, нет TLS и DNS, один поток (запросы обрабатываются по очереди). Для демонстрации и внутренних утилит достаточно, для интернета — нет.
Чего в языке нет
— Linux x86-64 only. Нужны as и ld; — нет исключений (try), нет пользовательских дженериков, нет пространств имён (импорт работает как include); — лямбды не захватывают переменные — замыканий нет; — целые числа 64-битные, при переполнении «заворачиваются»; длинной арифметики нет; — GC однопоточный и консервативный; — сеть: только TCP/IPv4, без TLS/DNS, один поток.
Что дальше
Из того, что хочется: собственный ассемблер и ELF-линкер (чтобы убрать зависимость от binutils), замыкания, исключения, раскрутка рекурсии и более умный выбор cmov, больше платформ. Если у вас есть идеи или желание помочь — репозиторий открыт.
Попробовать
Нужны Linux x86-64, Rust и GNU binutils:
git clone https://github.com/kuanysh228/tulpar
cd tulpar
cargo build —release
./install.sh # (по желанию) положит tulpar в ~/.local/bin
tulpar run файл.tlp # скомпилировать и запустить
tulpar build файл.tlp -o prog # автономный статический ELF
tulpar asm файл.tlp # посмотреть ассемблер
tulpar interp файл.tlp # запустить в интерпретаторе
Для VS Code в editors/vscode есть подсветка синтаксиса .tlp. README есть на казахском, русском и английском. Лицензия — MIT.
Репозиторий: https://github.com/kuanysh228/tulpar
Буду рад вопросам и критике в комментариях — особенно от тех, кто писал бэкенды компиляторов и увидит в моём regalloc.rs или opt.rs то, что я сделал наивно.
Автор: undercoverfts
