/
Поиск
RU

Под капотом: как устроен klc

Чтобы писать на kola, эта страница не нужна. Но вопросы «а есть ли тут интерпретатор?», «какие бэкенды и таргеты?», «можно ли звать сишные библиотеки?» задают все — вот ответы одним местом.

Два исполнителя одной программы

У klc два способа исполнить вашу программу, и это осознанная архитектура, а не переходный период:

  • klc run / klc test — интерпретатор. Обходит дерево программы и исполняет его напрямую. Медленный, зато служит эталоном семантики: именно он определяет, «как язык должен себя вести», и дополнительно проверяет в рантайме то, что статика уже доказала (например, теневой учёт линейности ресурсов).

  • klc cbuild / crun / ctest — нативная сборка. Компилятор генерирует C-код плюс маленький рантайм-заголовок kola.h, системный C-компилятор собирает объектник. crun запускает main, ctest гоняет те же test-блоки уже в машинном коде.

Оба исполнителя постоянно сверяются друг с другом на всём корпусе тестов: один и тот же тест обязан дать одинаковый результат в интерпретаторе и в нативе. Такое дифференциальное тестирование — главный способ, которым проект ловит баги самого компилятора.

Насколько быстрый натив: на горячем цикле (FNV-хеш по байтам) нативный kola идёт вровень с Rust — 1.10 против 1.10 GB/s в свежем замере — и в тысячи раз быстрее интерпретатора. При этом проверки переполнения и границ никуда не деваются: это trap, а не UB. Измеренный факт: никогда не срабатывающие проверки почти бесплатны — за безопасность здесь платить практически нечем.

Честности ради: паритет — не на всех формах кода. Там, где Rust умеет сам превратить проход по слайсу в SIMD-инструкции (автовекторизация), C-бэкенд пока отстаёт — до ~2.8x на потоковом суммировании диапазона. Причина понятна: сгенерированному C не хватает подсказки restrict, и C-компилятор векторизует осторожнее; задача заведена. Весь замер — пять нагрузок через все бэкенды против эталона на Rust — лежит в docs/perf-notes.md и перезапускается одной командой (cargo run --release --example perf_corpus -p klc-native).

Компилятор доказывает, а не надеется

Внутри klc есть типизированное промежуточное представление (KIR) и небольшой встроенный прувер линейной арифметики. Каждая проверка границ (xs[i]) решается один раз: если из req-контрактов и условий циклов следует, что индекс валиден, — проверка помечается «доказана» и в нативном коде исчезает; если не следует — остаётся дешёвой рантайм-проверкой. Решение записано в IR как факт, и исполнители обязаны ему следовать; ложное «доказано» — это внутренняя ошибка компилятора с громкой остановкой, а не тихая порча памяти.

Второй нативный бэкенд: Cranelift

Кроме C-бэкенда есть klc-clif — генерация машинного кода через Cranelift, без участия C-компилятора вообще. Это единственная внешняя зависимость проекта, и она изолирована в отдельный crate: ядро klc по-прежнему собирается без единой зависимости. Зачем он нужен: воспроизводимая сборка без системного cc и кросс-компиляция — с macOS собирается корректный ELF-объектник под aarch64-linux одним флагом --triple.

Третий бэкенд: свой машинный код (klc-native)

Есть и третий путь к машинному коду — самый радикальный. klc-native пишет машинные инструкции и объектные файлы сам, байт за байтом: без C-компилятора, без Cranelift, без единой внешней зависимости. Зачем, если два бэкенда уже есть? Три причины — и ни одна не про скорость:

  1. Криптография в константном времени. У любого оптимизирующего компилятора — clang, gcc, Cranelift — есть право «улучшить» безветвенный криптокод в ветвление по секрету, и флага «не делай так» не существует; такие поломки случались в настоящих криптобиблиотеках. Единственная честная гарантия — генератор, который ничего не оптимизирует: какие инструкции написали, такие и исполняются. Это опора для ct fn — константновременного подмножества kola.

  2. Автономия. Объектник рождается вообще без внешнего тулчейна — важно для bare-metal и для будущего самодостаточного компилятора: kola-компилятор, написанный на kola, не сможет позвать ни Rust-библиотеку, ни системный cc. Rust-версия этого бэкенда — эталон, с которым будущий kola-порт будет сверяться байт-в-байт.

  3. Ещё один независимый исполнитель той же программы. Дифференциальное тестирование — главный способ ловли багов компилятора, и каждый новый исполнитель реально их находит: гейт LoongArch-бэкенда, например, вскрыл расхождение семантики сдвигов на граничных значениях, общее для всех машинных бэкендов.

Сейчас klc-native покрывает пять архитектур и два объектных формата (Mach-O и ELF). Каждая инструкция каждого энкодера сверена байт-в-байт с ассемблером LLVM, а каждый бэкенд проверяется исполнением: значения и тексты трапов обязаны совпасть с интерпретатором-эталоном.

Архитектура

Где встречается

Как проверяется

aarch64

Apple Silicon, серверные ARM

исполнение прямо в тестах (JIT) + линковка объектников

x86-64

ПК и серверы

исполнение под Rosetta

RISC-V rv64

открытая архитектура, новое встраиваемое железо

qemu в docker-контейнере

MIPS64

роутеры, легаси-железо

qemu в docker-контейнере

LoongArch64

процессоры Loongson

кросс-сборка + qemu в docker-контейнере

Трапы во всех этих объектниках зовут тот же символ k_panic, что и C-бэкенд, — объектники разных бэкендов взаимозаменяемы при линковке. Плата за простоту: klc-native пока не распределяет регистры — каждое промежуточное значение ходит через память, поэтому он в 3–14 раз медленнее Rust (замерено тем же перф-корпусом). Это осознанный порядок работ: сначала корректность, потом распределитель регистров — приёмочный тест для него уже написан. В нишах этого бэкенда — крипто и bare-metal — скорость не решает.

Таргеты: от macOS до голого железа — и до браузера

  • Hosted — обычные macOS/Linux: cbuild даёт объектник, который линкуется как любой C-код.

  • Bare-metal — klc cbuild --freestanding: объектники вообще без libc. Куча — статический bump-аллокатор, печать и часы — внешние хуки, которые линкуются только если реально используются. Чистой программе нужен ровно один внешний символ — обработчик паники k_panic. Слинкованная с -nostdlib и собственным _start, программа работает на голом железе — и это условие существования языка: домашний стек kola — гипервизор и системные сервисы.

  • Браузер — сам интерпретатор собирается в WebAssembly (crates/klc-wasm): в репозитории лежит web/playground — страница с редактором, примерами практикума и кнопками «Проверить / Запустить / Тесты». Код исполняется локально в вашем браузере, std встроена в бандл, сервер не нужен.

Си и libc: можно, но громко

Вызывать C-функции можно: extern "C"-объявления существуют. Но с тремя жёсткими условиями. Во-первых, объявление несёт полную kola-сигнатуру — эффекты и контракты, ничего неявного. Во-вторых, сам вызов возможен только внутри блока trusted "почему" { ... }, где строка-обоснование обязательна, а req-контракты обязаны доказываться статически. В-третьих, каждый trusted-блок — строка в аудите: klc audit их считает, и пакет с [trusted] allowed = false в манифесте с таким кодом просто не соберётся.

Почему так строго: за FFI-границей все гарантии kola заканчиваются. Эффекты, контракты, линейность — всё это компилятор проверить внутри чужого .so не может, он верит вашей сигнатуре на слово. Поэтому граница с C сделана дорогой и заметной: её видно в коде (trusted), в манифесте и в аудите. Сам же рантайм kola в libc не нуждается — см. bare-metal выше.

Редакторы и инструменты — честный статус

LSP-сервера и плагинов для IDE пока нет. Рабочий цикл — любой редактор плюс klc check: диагностики структурные, цитируют раздел карты и предлагают исправление — проект сознательно вкладывается в них как в основной интерфейс компилятора. Побаловаться без установки можно в браузерной песочнице web/playground (см. выше). Ещё есть klc query outline|symbol|callers|callees|effects — карта кода в JSON из фактов компилятора (сигнатуры, эффекты, граф вызовов); это готовая основа для будущей интеграции с редакторами и агентами.