- PVSM.RU - https://www.pvsm.ru -

Психанул: как я за полтора месяца сделал формат конфигов и парсеры для семи языков

Зуд

Я ковырял pet-проект — ротатор SOCKS5-прокси — и в очередной раз редактировал конфиг апстримов руками. Открыл файл, посмотрел на JSON:

Психанул: как я за полтора месяца сделал формат конфигов и парсеры для семи языков - 1

Кавычки вокруг каждого ключа. Кавычки вокруг каждой строки. Невозможно просто вставить список кредов от провайдера. Запятые после каждой строчки. Забыл запятую — ошибка парсинга указывает не на ту строку. Я тратил больше времени на пунктуацию, чем на сами значения.

Переписал на YAML — через двадцать минут вырезал случайно сдвинутый на пробел блок. Ощущения — как в комсмосе. Не понятно в каком блоке я нахожусь, страшно править. Переписал на TOML — [[upstreams]] для массива объектов оказался неудобным ровно в тот момент, когда объекты нужно было вложить ещё на уровень.

Дальше случилось то, что я обычно советую не делать: я не выбрал один из существующих 14 форматов, а сделал 15-й.

22 апреля 2026 года появился первый коммит спеки — 0.1.0. Через полтора месяца, 5 июня, экосистема из десяти репозиториев была на 0.6.1, полностью опубликована в семь пакетных реестров. Этот пост — не столько про сам формат, сколько про то, что оказалось по-настоящему сложным: как раскатать один парсер на семь языков так, чтобы его поведение нигде не разъехалось.

Что такое Ktav

Название — כְּתָב, «письмо» на иврите. Идея простая: взять модель данных JSON (скаляры, массивы, объекты, null, булевы) и снять с неё пунктуацию, которая мешает писать руками.

Психанул: как я за полтора месяца сделал формат конфигов и парсеры для семи языков - 2

Ни кавычек, ни запятых, осттупы не влияют на структуру данных. Голое число, похожее на целое, становится Integer; похожее на дробное — Float;true/false/null — ключевые слова-значения, всё остальное — строка .

Массивы и объекты необязательно растягивать на несколько строк — если запись помещается в одну, можно (и часто удобнее) написать её однострочно, через запятую:

Психанул: как я за полтора месяца сделал формат конфигов и парсеры для семи языков - 3

Здесь запятые — необходимость: в блочной, многострочной форме роль разделителя элементов играет перенос строки, и она же убирает нужду в запятых. Но когда всё на одной строке, переноса нет — запятая берёт эту роль на себя. Оба стиля дают одинаковое дерево значений, но и их можно свободно смешивать.

Следующие решения здесь — сознательные компромиссы:

  • ## вместо # для комментариев. Одиночная решётка слишком часто встречается внутри значений — hex-цвета, номера issue, имена каналов. color: #ff5577 парсится без экранирования именно поэтому.

  • :: — «форсировать строку». Когда типизация по форме ошибается (например, я хочу, чтобы "true" осталась строкой, а не булевым, или чтобы 00544 не превратилось в 544), :: — явный флаг «бери как есть».

  • Многострочные строки через ( … ) с авто-отбивкой отступа — вместо |/>. А так же (( … )) - для сохранения отступов.

  • Точечные ключи (node.host [1]: a.example) — синтаксический сахар для вложенности. Единственное место в формате, где есть «два способа сделать одно и то же»; я долго колебался, оставлять ли, мне опказалось это очень удобным.

Чего в формате нет: якорей и ссылок (&/* из YAML), тегов типов, выражений, интерполяции, схемы. Каждая отсутствующая фича — решение в пользуй минимализма.

Постановка настоящей задачи

Формат, который живёт на одном языке, — игрушка. Как только над одним конфигом работает полиглот-команда (сервис на Go, тулинг на Python, дашборд на JS), «формат конфига» обязан значить одно и то же везде, байт в байт. Есть три способа это провалить:

  1. Переписать парсер на каждом языке — N реализаций, N чуть разных диалектов. Именно так фрагментируется экосистема YAML: где-то 1.0 — строка, где-то float, где-то по-разному трактуются якоря.

  2. Один эталонный парсер, но склейка с языками настолько рыхлая, что каждый биндинг обрастает своими причудами.

  3. Один парсер, один источник правды, и тест-сьют, который доказывает, что все биндинги согласны.

Я выбрал третий вариант, и именно это оказалось интереснее самого формата.

Ядро на Rust, один C ABI и WASM в браузере

Эталонный парсер — крейт ktav на Rust: recursive descent, без генератора парсеров, zero-copy где возможно, нативная интеграция с serde.

Психанул: как я за полтора месяца сделал формат конфигов и парсеры для семи языков - 4

Поверх ядра — тонкий C ABI (ktav_cabi): несколько extern "C" функций с явным контрактом владения памятью (парсер аллоцирует, вызывающая сторона освобождает через _free-функцию; строки пересекают границу как байтовые срезы с длиной, а не null-terminated C-строки). Все языковые биндинги — просто разные способы говорить с этим ABI:

Язык

Механизм FFI

Как ставится

JS/TS

N-API (нативно) + WASM (фолбэк)

npm i @ktav-lang/ktav

Python

PyO3 + abi3-wheels

pip install ktav

Go

purego, без cgo для потребителя

go get github.com/ktav-lang/golang

PHP

ext-ffi (PHP 7.4+)

composer require ktav-lang/ktav

Java

JNA, без JNI для потребителя

Maven Central

C#/.NET

P/Invoke

dotnet add package Ktav

Отдельно хочу выделить WASM — это же самое Rust-ядро, тот же самый код, что гоняет тесты и обслуживает продовые парсинги, компилируется в WebAssembly и крутится прямо в браузере на лендинге — без сервера, без бэкенда, без парсинга на js. Вставил в playground YAML или TOML — получил Ktav.

Самыми неочевидными оказались не сами обёртки, а протаскивание ошибок через границу: Result в Rust на ABI-уровне становится размеченным объединением (код + позиция + сообщение), а дальше каждый язык заворачивает это в свою идиому — исключение в Python/Java/C#, error в Go, отклонённый промис в JS.

Как держим биндинги: тест-спека

C ABI даёт одну реализацию, но у каждого биндинга остаётся собственная поверхность: кодировка строк, маппинг ошибок, поведение на границах чисел. Поэтому спека — это отдельная папка tests/ с фикстурами прямо в репозитории, версионированная вместе с грамматикой (versions/0.6/tests/). В актуальной версии там 374 файла, разложенных по valid/ (массивы, объекты, точечные ключи, комментарии, многострочные строки, инлайн-компаунды, экранирование ключей, числа…) и invalid/ (что обязано падать: дубли ключей, незакрытые скобки, битое экранирование, конфликт ключевого пути).

Каждый тестовый кейс — это тройка файлов: .ktav (вход), .json (ожидаемое дерево значений) и *.canonical.ktav (эталонный round-trip — во что должен превратиться этот вход, если распарсить и сериализовать обратно). То есть проверяется не только «правильно ли распарсили», но и «правильно ли записали canonical-форму назад» — это ловит асимметрии вроде «прочитали инлайн-объект, а на выходе почему-то развернули его в блочный».

Именно эту тройку файлов каждый из семи биндингов гоняет в своём собственном CI против своей собственной реализации. Биндинг у меня не считается готовым, когда он компилируется. Он готов, когда проходит тот же самый набор фикстур, что и эталон, на своём языке. «Все тесты спеки зелёные на 7 языках» — это доказательство, которое я предъявляю своему внутреннему скептику вместо слов «работает же».

Тулинг для редакторов

  • LSP-сервер (ktav-lsp, отдельный крейт на Rust) — диагностика, автодополнение, hover.

  • Плагин VS Code и плагин JetBrains (IntelliJ, RustRover, PyCharm, WebStorm, GoLand, PhpStorm, Rider) — оба бандлят LSP и подсветку.

  • Грамматика tree-sitter — для Neovim, Helix, Zed и любого другого редактора с поддержкой tree-sitter.

Плагины так и не опубликовал. На сайте MS заблудился и меня они забанили, похоже, за количество переходов по их ссылкам. JetBrains - устал модерацию проходить.

Где можно потыкать

Всё написанное — open-source, dual-licensed MIT OR Apache-2.0. Попробовать без установки: ktav-lang.github.io [2] (playground на WASM, конвертирует JSON/YAML/TOML/INI ⇄ Ktav прямо в браузере, ничего не уходит на сервер).

Код — github.com/ktav-lang [3].

Автор: CraftDream

Источник [4]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/javascript/456871

Ссылки в тексте:

[1] node.host: http://node.host

[2] ktav-lang.github.io: http://ktav-lang.github.io

[3] github.com/ktav-lang: https://github.com/ktav-lang

[4] Источник: https://habr.com/ru/articles/1072042/?utm_source=habrahabr&utm_medium=rss&utm_campaign=1072042